Skip to main content

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:

Architecture: MCP server as CSE plugin

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

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

Layered security architecture

  • 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-aCagenta) — 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

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

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

onem2m_mcp-0.3.0.tar.gz (403.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

onem2m_mcp-0.3.0-py3-none-any.whl (30.4 kB view details)

Uploaded Python 3

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

Hashes for onem2m_mcp-0.3.0.tar.gz
Algorithm Hash digest
SHA256 8bab2302e8298c62b4319704a05a236fb5c681cad1217d939dafa0f458b1dacd
MD5 d4b7969ba65f7b95233d1ed4f138b182
BLAKE2b-256 5c985279ad868fdc03b82efa9707e9ed8e8a39fd950df41d50e772b2bd8dd154

See more details on using hashes here.

Provenance

The following attestation bundles were made for onem2m_mcp-0.3.0.tar.gz:

Publisher: publish.yml on rostanai/oneM2M_MCP

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for onem2m_mcp-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0f8d63ad583d20717fd8c0d67e0a6abb7e1de26083395cf1df71746c72c6de74
MD5 07a32f06b40f9ab5aae6daf3bc8f9936
BLAKE2b-256 2b49085ac2c55c68852a89d668b17d528b409c9ee24915a22e8c0af6c8b70766

See more details on using hashes here.

Provenance

The following attestation bundles were made for onem2m_mcp-0.3.0-py3-none-any.whl:

Publisher: publish.yml on rostanai/oneM2M_MCP

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page