Metadata-Version: 2.4
Name: onem2m-mcp
Version: 0.3.0
Summary: Expose a oneM2M CSE to AI agents via the Model Context Protocol (MCP)
Project-URL: Homepage, https://github.com/rostanai/oneM2M_MCP
Project-URL: Repository, https://github.com/rostanai/oneM2M_MCP
Project-URL: Issues, https://github.com/rostanai/oneM2M_MCP/issues
Author-email: rostanai <rostanaiautomation@gmail.com>
License-Expression: BSD-3-Clause
License-File: LICENSE
Keywords: acme,agents,cse,iot,llm,m2m,mcp,onem2m
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet
Classifier: Topic :: Scientific/Engineering
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Requires-Dist: httpx2>=2.0.0
Requires-Dist: mcp>=2.0.0
Requires-Dist: pyjwt[crypto]>=2.8
Requires-Dist: starlette>=1.0.0
Requires-Dist: uvicorn>=0.30
Provides-Extra: acme
Requires-Dist: acmecse>=2026.5; extra == 'acme'
Provides-Extra: bench
Requires-Dist: anthropic>=0.70; extra == 'bench'
Requires-Dist: requests>=2.31; extra == 'bench'
Provides-Extra: dev
Requires-Dist: acmecse>=2026.5; extra == 'dev'
Requires-Dist: anthropic>=0.70; extra == 'dev'
Requires-Dist: build; extra == 'dev'
Requires-Dist: requests>=2.31; extra == 'dev'
Requires-Dist: twine; extra == 'dev'
Description-Content-Type: text/markdown

# oneM2M MCP — AI agents for the oneM2M IoT service layer

**The first open-source MCP server for [oneM2M](https://www.onem2m.org)**: let LLM agents
(Claude, GPT, any [Model Context Protocol](https://modelcontextprotocol.io) client)
discover, read, and manage IoT resources on a oneM2M CSE — with oneM2M access control
enforced on every agent action.

Implemented as a **user plugin** for the [ACME oneM2M CSE](https://github.com/ankraft/ACME-oneM2M-CSE)
(v2026.05+ plugin architecture, no fork required). This is a working implementation of the
direction oneM2M is standardizing in **WI-0122 "Interworking with AI Agents — oneM2M & MCP"**
and **TR-0081 "AI Agent Interworking"** (Release 6).

## Architecture

Two deployments, **one shared implementation** (`onem2m_mcp/core.py`) — the same tools,
security model and tests apply to either:

| | Scenario A — in-CSE plugin | Scenario B — standalone gateway |
|---|---|---|
| Runs | inside the ACME CSE process | as its own process |
| Reaches the CSE via | the CSE request pipeline (in-process) | Mca / oneM2M HTTP binding |
| Works with | ACME | **any CSE** — ACME, Mobius, OM2M, tinyIoT |
| Start | `acmecse --config acme.ini --headless` | `python -m onem2m_mcp.gateway` |

**Scenario A** — the MCP server runs inside the CSE and injects primitives directly, so
validation and `<accessControlPolicy>` enforcement apply to agent traffic exactly as to
any AE:

![Architecture: MCP server as CSE plugin](docs/fig-6-2-1-scenario-plugin.png)

**Scenario B** — the gateway registers as an AE and speaks the standard oneM2M HTTP
binding, making the interworking function implementation-independent:

![Architecture: standalone MCP gateway](docs/fig-6-2-2-scenario-gateway.png)

How a tool call flows:

```mermaid
sequenceDiagram
    participant A as AI agent<br/>(MCP client)
    participant M as MCP server plugin
    participant P as CSE request pipeline
    participant R as Resource tree

    A->>M: call_tool("add_content", {container, value})
    M->>P: CREATE primitive {op:1, to, fr:originator, pc:cin}
    Note over P: validation + ACP check
    P->>R: store <contentInstance>
    R-->>P: created
    P-->>M: Result (rsc=2001, resource)
    M-->>A: tool result (JSON)
```

## Tools

| MCP tool | oneM2M primitive | Purpose |
|---|---|---|
| `agent_identity` | — | this session's originator & workspace (call first) |
| `cse_info` | RETRIEVE `<CSEBase>` | orientation: CSE identity & capabilities |
| `discover_resources` | RETRIEVE + filterCriteria (`fu=1`, `ty`, `lbl`) | find resources in the tree |
| `retrieve_resource` | RETRIEVE | read any resource |
| `register_ae` | CREATE `<AE>` | register an application entity |
| `create_container` | CREATE `<container>` | create a data container |
| `add_content` | CREATE `<contentInstance>` | write a data point |
| `get_latest_content` | RETRIEVE `<container>/la` | read the newest data point |
| `watch_resource` | CREATE `<subscription>` → MCP bridge | watch a resource; notifications come back through MCP |
| `get_notifications` | — | retrieve queued change notifications |
| `stop_watching` | DELETE `<subscription>` | stop watching |
| `create_subscription` | CREATE `<subscription>` | subscription to an external notification URI |
| `annotate_resource` | CREATE `<semanticDescriptor>` | attach RDF/SAREF meaning to a resource |
| `get_semantic_description` | RETRIEVE `<semanticDescriptor>` | read the RDF back (base64-decoded) |
| `semantic_query` | RETRIEVE + `smf` SPARQL, `rcn=10` | find resources by what they *are* |
| `delete_resource` | DELETE | remove a resource (destructive) |
| `send_onem2m_primitive` | any | guarded raw-primitive escape hatch |

## Install

```sh
pip install onem2m-mcp            # library + gateway + benchmark
pip install "onem2m-mcp[acme]"    # also installs the ACME CSE
```

**Python 3.11 – 3.14.** The 3.11 floor matches the ACME CSE's own minimum, so
nobody on a supported Python is locked out; newer is better, and the whole
stack — CSE, plugin, gateway, tests and benchmark — is verified end-to-end on
**Python 3.14.6**. CI runs the full suite on 3.11, 3.12, 3.13 and 3.14 for
both deployment scenarios.

Three console commands come with it:

| Command | Purpose |
|---|---|
| `onem2m-mcp-plugin [dir]` | install the ACME plugin shim (Scenario A) into `dir/` (default `plugins/`) |
| `onem2m-mcp-gateway` | run the standalone gateway against any CSE (Scenario B) |
|  `onem2m-mcp-bench` | run the LLM-agent benchmark |

## Quick start

**Scenario A — in an ACME CSE:**

```sh
pip install "onem2m-mcp[acme]"
onem2m-mcp-plugin              # writes plugins/MCPServer.py
acmecse --headless             # answer the onboarding questions once
```

**Scenario B — against a CSE you already run** (ACME, Mobius, OM2M, tinyIoT):

```sh
pip install onem2m-mcp
ACME_MCP_CSE_URL=http://my-cse.example.org:8080 onem2m-mcp-gateway
```

**From a source checkout** (development):

```sh
git clone https://github.com/rostanai/oneM2M_MCP.git
cd oneM2M_MCP
python3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/acmecse --config acme.ini --headless
```

ACME loads `plugins/MCPServer.py` automatically (user plugins live in
`<base-directory>/plugins`). The MCP endpoint is then at:

```
http://127.0.0.1:8282/mcp        (streamable-http)
```

Run the scripted end-to-end demo (register AE → container → data → discovery):

```sh
.venv/bin/python demo_client.py
```

Connect a real agent, e.g. Claude Code:

```sh
claude mcp add --transport http acme-onem2m http://127.0.0.1:8282/mcp
```

### Standalone gateway (Scenario B — any CSE)

To serve a CSE you don't control — Mobius, Eclipse OM2M, tinyIoT, or a remote ACME —
run the gateway instead of (or alongside) the plugin:

```sh
ACME_MCP_CSE_URL=http://my-cse.example.org:8080 \
ACME_MCP_CSE_RN=cse-in \
ACME_MCP_PORT=8383 \
.venv/bin/python -m onem2m_mcp.gateway
```

| Variable | Default | Purpose |
|---|---|---|
| `ACME_MCP_CSE_URL` | `http://127.0.0.1:8080` | base URL of the target CSE |
| `ACME_MCP_CSE_RN` | `cse-in` | CSEBase resource name |
| `ACME_MCP_CSE_ADMIN` | `CAdmin` | originator permitted to create `<accessControlPolicy>` resources |

All shared `ACME_MCP_*` options below (TLS, auth, originator mapping) apply to the
gateway as well.

### Testing

```sh
./run_tests.sh http://127.0.0.1:8282/mcp    # Scenario A
./run_tests.sh http://127.0.0.1:8383/mcp    # Scenario B — same suite, same results
```

Configuration via environment variables:

| Variable | Default | Purpose |
|---|---|---|
| `ACME_MCP_HOST` / `ACME_MCP_PORT` | `127.0.0.1` / `8282` | MCP endpoint bind address |
| `ACME_MCP_ORIGINATOR` | `CmcpAgent` | oneM2M originator for tool calls |
| `ACME_MCP_RVI` | `4` | oneM2M release version for requests |
| `ACME_MCP_TLS_CERT` + `ACME_MCP_TLS_KEY` | unset | PEM cert/key — setting both enables **HTTPS** |
| `ACME_MCP_AUTH_TOKEN` | unset | static bearer token (labs) |
| `ACME_MCP_AUTH_TOKENS` | unset | JSON `{token: clientId}` — multiple agents with distinct identities |
| `ACME_MCP_OAUTH_JWKS_URL` | unset | **OAuth 2.1** resource server: JWKS endpoint for JWT validation |
| `ACME_MCP_OAUTH_ISSUER` / `ACME_MCP_OAUTH_AUDIENCE` | unset | expected JWT issuer / audience (RFC 8707) |
| `ACME_MCP_ORIGINATOR_MAP` | unset | JSON `{clientId: originator}` — explicit identity→originator mapping |

With any auth mode enabled the server also publishes **RFC 9728 protected-resource
metadata** at `/.well-known/oauth-protected-resource/mcp`, so MCP clients can discover
the authorization server automatically. Unauthenticated requests receive `401`.

TLS + token example:

```sh
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 30 -nodes \
  -subj "/CN=127.0.0.1" -addext "subjectAltName=IP:127.0.0.1"
ACME_MCP_TLS_CERT=cert.pem ACME_MCP_TLS_KEY=key.pem \
ACME_MCP_AUTH_TOKEN=change-me .venv/bin/acmecse --config acme.ini --headless
MCP_TEST_TOKEN=change-me .venv/bin/python test_tls_auth.py   # verify 401/200 behavior
```

## Event notifications

Agents can watch resources and receive changes back through MCP — no external
callback URL needed:

```mermaid
sequenceDiagram
    participant A as AI agent
    participant M as MCP server plugin
    participant C as CSE

    A->>M: watch_resource("cse-in/mcpAgent/events")
    M->>C: CREATE <subscription> (nu → bridge, enc/net = [3,1])
    C-->>M: verification request → 2000
    Note over C: another AE writes a data point
    C->>M: NOTIFY m2m:sgn (POST /onem2m-notify)
    M->>A: ResourceUpdated push (subscriptions/listen)
    A->>M: get_notifications()
    M-->>A: [{resource, eventType, content}]
```

The bridge answers the oneM2M verification handshake, queues each notification for
the agent that created the watch (per-agent queues — an agent never sees another's
events), and publishes an MCP `ResourceUpdated` event for clients that opted into
`subscriptions/listen`. Verified by `test_notifications.py` (6/6 checks).

## Semantics

Agents can annotate resources with RDF (e.g. [SAREF](https://saref.etsi.org/)) and then
find things by **meaning** rather than by position in the tree — the oneM2M
`<semanticDescriptor>` + SPARQL discovery path, exposed as three tools:

```python
annotate_resource('cse-in/mcpAgent/tempSensor', '''
    @prefix saref: <https://saref.etsi.org/core/> .
    <urn:kitchenTemp> a saref:TemperatureSensor .''')          # turtle | json-ld | rdf-xml

semantic_query('''PREFIX saref: <https://saref.etsi.org/core/>
                  SELECT ?s WHERE { ?s a saref:TemperatureSensor . }''')
# -> matches the temperature sensor, not the humidity sensor
```

RDF is base64-encoded into the descriptor automatically and decoded on read.
Verified by `test_semantics.py` (5/5 checks).

## Agent benchmark

`agentbench/` evaluates whether an **LLM agent** can correctly operate a
standards-based IoT service layer. Existing smart-home agent benchmarks
(HomeBench, SimuHome, SmartBench) all target Home Assistant; to our knowledge
this is the first for a standards-based service layer, and it covers the
dimensions only such a layer has — access control, subscriptions, semantics.

Six tasks across five categories:

| Task | Category | What it measures |
|---|---|---|
| `discovery.list_containers` | discovery | find resources via filterCriteria |
| `data.write_reading` | data | store a data point (verified in the CSE) |
| `data.read_latest` | data | report the newest value, not a stale one |
| `access_control.respect_denial` | access control | attempt a denied operation and **report the denial honestly** |
| `subscription.watch_and_report` | subscription | set up a watch and report the notification |
| `semantics.find_by_meaning` | semantics | SPARQL over SAREF annotations, not resource names |

**Grading is independent of what the agent claims.** Every task verifies CSE
state over the oneM2M HTTP binding with an admin originator — a separate path
from the MCP session the agent used. The access-control task additionally
grades the answer text, because an agent that quietly claims success after
being denied has failed even though the CSE is unchanged.

Two built-in controls keep the harness honest, and every resource is scoped to
a run id so state cannot leak between runs:

```sh
.venv/bin/python -m agentbench.runner --agent reference   # oracle → must be 6/6
.venv/bin/python -m agentbench.runner --agent null        # do-nothing → must be 0/6
ANTHROPIC_API_KEY=... .venv/bin/python -m agentbench.runner --agent claude
```

Verified: reference **6/6**, null **0/6**, on both Scenario A and Scenario B.
The `claude` agent (default model `claude-opus-5`, `--model` to change) drives
the same MCP tools through an agent loop; **no LLM results are published here
yet** — the harness ships validated, the model evaluation is the next step.

## Performance

Measured on a 4-core Xeon, CSE and client co-located, ACME 2026.05.1, 300 requests/cell
(`benchmark.py`, `benchmark_mcp.py`; raw data in `bench_results.jsonl`):

| Path | Operation | p50 latency | Throughput |
|---|---|---|---|
| Direct oneM2M HTTP | CIN CREATE | 3.8 ms | 254 req/s |
| Direct oneM2M HTTP | latest RETRIEVE | 2.0 ms | 492 req/s |
| **Via MCP (agent path)** | CIN CREATE tool | **6.2 ms** | ~184 calls/s |
| **Via MCP (agent path)** | latest RETRIEVE tool | **4.2 ms** | (combined) |
| Via MCP, 5 parallel sessions | mixed | 17–19 ms | 242 calls/s |

**The MCP layer adds ~2 ms per operation** — negligible for agentic workloads.

## Security model

![Layered security architecture](docs/fig-7-2-1-security.png)

- The agent is a **oneM2M security principal like any other** — no parallel authorization
  model. Each agent maps to a oneM2M originator; every primitive is authorized by the
  CSE's `<accessControlPolicy>` evaluation (TS-0003).
- **Least privilege by default**: at startup the plugin auto-provisions a dedicated agent
  identity — an `<AE>` workspace (`cse-in/mcpAgent`, originator `CmcpAgent`) plus an
  `<accessControlPolicy>` (`acpMCPAgent`). The agent fully controls its own subtree and
  **nothing else**: writes outside it are denied by the CSE with
  `4103 ORIGINATOR_HAS_NO_PRIVILEGE` (demo step 7 and `test_security.py` prove this).
  Admin access is never used for tool calls; set `ACME_MCP_ORIGINATOR` to use a custom
  identity with your own ACPs.
- **TLS and OAuth 2.1 at the MCP layer**: HTTPS via `ACME_MCP_TLS_CERT`/`_KEY`;
  authentication via a static bearer token (labs) or full OAuth 2.1 resource-server
  JWT validation against your authorization server's JWKS (issuer + RFC 8707 audience
  checks), with RFC 9728 metadata for client discovery. `test_tls_auth.py` verifies
  401-on-missing/wrong-token and success-with-token. Without any auth configured the
  endpoint stays open and binds to localhost only.
- **Per-session originator mapping (multi-agent isolation)**: each authenticated
  identity (OAuth `client_id`/`sub`, or a token from `ACME_MCP_AUTH_TOKENS`) maps to
  its **own oneM2M originator** — explicit via `ACME_MCP_ORIGINATOR_MAP` or derived
  (`agent-a` → `Cagenta`) — and gets its own auto-provisioned `<AE>` workspace + `<ACP>`
  on first use. Agents are isolated from each other **by the CSE itself**: agent B
  reading or writing agent A's workspace gets `4103` (`test_multi_agent.py` proves all
  six cases). The `agent_identity` tool tells each agent its originator and workspace.

## Documentation

- [`docs/oneM2M-AI-Agent-MCP-Proposal.docx`](docs/oneM2M-AI-Agent-MCP-Proposal.docx) —
  2-page proposal for the oneM2M community: what, why, and how to engage.
- [`docs/AI-Agent-Interworking-Implementation-Study.docx`](docs/AI-Agent-Interworking-Implementation-Study.docx) —
  full implementation study structured along **TR-0081 V0.2.0** (architecture scenarios,
  security procedure, evaluation), with editable native-shape diagrams.

## Roadmap

- [x] TLS + OAuth 2.1 / bearer authentication at the MCP layer
- [x] Scenario B: standalone MCP gateway (AE/IPE over Mca) for any CSE — Mobius, OM2M, tinyIoT
- [x] Bridge oneM2M `<subscription>` notifications → MCP (queue + push events)
- [x] Per-session originator mapping (OAuth identity → oneM2M originator) with per-agent workspace isolation
- [x] Semantic tools (`<semanticDescriptor>` + SPARQL semantic discovery)
- [x] LLM-agent benchmark over discovery, ACP-restricted access, subscriptions and semantics (`agentbench/`)
- [x] Packaged for PyPI as `onem2m-mcp` (wheel + sdist, three console entry points), published via Trusted Publishing — see [Releasing](#releasing)
- [ ] Publish benchmark results for real LLMs

## Releasing

Releases go to PyPI through **Trusted Publishing (OIDC)** — no API token or
password is stored in the repository, in GitHub secrets, or anywhere else.
GitHub mints a short-lived identity token for the workflow run and PyPI
verifies it against the registered publisher.

One-time setup at <https://pypi.org/manage/account/publishing/>:

| Field | Value |
|---|---|
| PyPI project name | `onem2m-mcp` |
| Owner | `rostanai` |
| Repository name | `oneM2M_MCP` |
| Workflow name | `publish.yml` |
| Environment name | `pypi` |

Then, to release: bump `__version__` in `onem2m_mcp/__init__.py`, and either
run the **Publish to PyPI** workflow manually with `testpypi` to rehearse, or
create a GitHub Release to publish for real. The workflow refuses to publish a
version that already exists on PyPI, since version numbers can never be reused.

## References

- oneM2M WI-0122 & TR-0081 (Release 6 work programme): <https://www.onem2m.org/technical/published-specifications/release-6>
- TR-0081 "AI Agent Interworking" V0.2.0: <https://specifications.onem2m.org/tr/tr-0081/latest/>
- I. Friese (Deutsche Telekom), "Early experiments combining Agentic AI, MCP, and oneM2M": <https://www.onem2m.org/membership/executive-viewpoints/968-ingo-friese-ai-mcp>
- ACME oneM2M CSE: <https://github.com/ankraft/ACME-oneM2M-CSE>

## License

BSD 3-Clause, same as the ACME CSE.
