oneM2M MCP — AI agents for the oneM2M IoT service layer
The first open-source MCP server for oneM2M: let LLM agents (Claude, GPT, any Model Context Protocol 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 (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:
Scenario B — the gateway registers as an AE and speaks the standard oneM2M HTTP binding, making the interworking function implementation-independent:
How a tool call flows:
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
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:
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):
pip install onem2m-mcp
ACME_MCP_CSE_URL=http://my-cse.example.org:8080 onem2m-mcp-gateway
From a source checkout (development):
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):
.venv/bin/python demo_client.py
Connect a real agent, e.g. Claude Code:
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:
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
./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:
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:
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) and then
find things by meaning rather than by position in the tree — the oneM2M
<semanticDescriptor> + SPARQL discovery path, exposed as three tools:
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:
.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
- 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, originatorCmcpAgent) plus an<accessControlPolicy>(acpMCPAgent). The agent fully controls its own subtree and nothing else: writes outside it are denied by the CSE with4103 ORIGINATOR_HAS_NO_PRIVILEGE(demo step 7 andtest_security.pyprove this). Admin access is never used for tool calls; setACME_MCP_ORIGINATORto 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.pyverifies 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 fromACME_MCP_AUTH_TOKENS) maps to its own oneM2M originator — explicit viaACME_MCP_ORIGINATOR_MAPor 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 gets4103(test_multi_agent.pyproves all six cases). Theagent_identitytool tells each agent its originator and workspace.
Documentation
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— full implementation study structured along TR-0081 V0.2.0 (architecture scenarios, security procedure, evaluation), with editable native-shape diagrams.
Roadmap
- TLS + OAuth 2.1 / bearer authentication at the MCP layer
- Scenario B: standalone MCP gateway (AE/IPE over Mca) for any CSE — Mobius, OM2M, tinyIoT
- Bridge oneM2M
<subscription>notifications → MCP (queue + push events) - Per-session originator mapping (OAuth identity → oneM2M originator) with per-agent workspace isolation
- Semantic tools (
<semanticDescriptor>+ SPARQL semantic discovery) - LLM-agent benchmark over discovery, ACP-restricted access, subscriptions and semantics (
agentbench/) - Packaged for PyPI as
onem2m-mcp(wheel + sdist, three console entry points), published via Trusted Publishing — see 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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file onem2m_mcp-0.3.0.tar.gz.
File metadata
- Download URL: onem2m_mcp-0.3.0.tar.gz
- Upload date:
- Size: 403.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8bab2302e8298c62b4319704a05a236fb5c681cad1217d939dafa0f458b1dacd
|
|
| MD5 |
d4b7969ba65f7b95233d1ed4f138b182
|
|
| BLAKE2b-256 |
5c985279ad868fdc03b82efa9707e9ed8e8a39fd950df41d50e772b2bd8dd154
|
Provenance
The following attestation bundles were made for onem2m_mcp-0.3.0.tar.gz:
Publisher:
publish.yml on rostanai/oneM2M_MCP
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
onem2m_mcp-0.3.0.tar.gz -
Subject digest:
8bab2302e8298c62b4319704a05a236fb5c681cad1217d939dafa0f458b1dacd - Sigstore transparency entry: 2343885057
- Sigstore integration time:
-
Permalink:
rostanai/oneM2M_MCP@58af019e3c4957eeec7122bb7065fb79b9f8f6b5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/rostanai
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@58af019e3c4957eeec7122bb7065fb79b9f8f6b5 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file onem2m_mcp-0.3.0-py3-none-any.whl.
File metadata
- Download URL: onem2m_mcp-0.3.0-py3-none-any.whl
- Upload date:
- Size: 30.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0f8d63ad583d20717fd8c0d67e0a6abb7e1de26083395cf1df71746c72c6de74
|
|
| MD5 |
07a32f06b40f9ab5aae6daf3bc8f9936
|
|
| BLAKE2b-256 |
2b49085ac2c55c68852a89d668b17d528b409c9ee24915a22e8c0af6c8b70766
|
Provenance
The following attestation bundles were made for onem2m_mcp-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on rostanai/oneM2M_MCP
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
onem2m_mcp-0.3.0-py3-none-any.whl -
Subject digest:
0f8d63ad583d20717fd8c0d67e0a6abb7e1de26083395cf1df71746c72c6de74 - Sigstore transparency entry: 2343885076
- Sigstore integration time:
-
Permalink:
rostanai/oneM2M_MCP@58af019e3c4957eeec7122bb7065fb79b9f8f6b5 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/rostanai
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@58af019e3c4957eeec7122bb7065fb79b9f8f6b5 -
Trigger Event:
workflow_dispatch
-
Statement type: