openrois-mcp
An MCP server that gives an AI agent the physical robots, virtual avatars, and AI services behind an OpenRoIS gateway, through the standard interfaces of the OMG RoIS Framework 2.0.
An agent such as Claude Code or Claude Desktop finds the components, reads their profiles,
reads state, follows events and, when you allow it, runs commands. It needs no knowledge of
any robot's own API. The server is built on
openrois-sdk.
Alpha, pre-1.0, unstable API.
Install
pip install openrois-mcp
From source:
git clone https://github.com/openrois/openrois.git
pip install -e ./openrois/services/mcp
The server needs Python 3.12 or later and a running gateway. The Docker Compose stack at the repository root serves simulated components through one:
docker compose up --build -d # a gateway on ws://127.0.0.1:8765 with the mock adapter
Use It from an Agent
Claude Code
claude mcp add openrois -- openrois-mcp --gateway-url ws://127.0.0.1:8765 --allow-actions
Leave out --allow-actions to let the agent look but not act.
Claude Desktop
Add the server to claude_desktop_config.json:
{
"mcpServers": {
"openrois": {
"command": "openrois-mcp",
"args": ["--gateway-url", "ws://127.0.0.1:8765", "--allow-actions"]
}
}
}
Give the full path of openrois-mcp in command if Claude Desktop does not find it on its
PATH.
Safety
The gateway does not authenticate connections in the alpha releases, and an agent with actions allowed can move a physical robot. The server limits what an agent can do:
- Read-only by default.
bind,release,set_parametersandrun_commandsexist only with--allow-actions. A read-only server binds nothing, not even forget_parameters. - An allow list.
--allow 'reachy_real/%', repeatable, limits the components an agent can bind, configure and command to the refs that match a CQL2 LIKE pattern. Discovery still shows every component. - Annotations. The tools that only read are marked read-only.
run_commands,set_parametersandreleaseare marked destructive, so an MCP client can ask you before it runs them. - A clean exit. When the agent closes the server, it runs
stopon the components this session started and did not stop, such as a person detector, ends its subscriptions, releases every bind, which aborts its running commands, and disconnects. Each step is best effort within five seconds, so a gateway that does not answer never holds up the exit. - Local by default. stdio needs no port. The streamable-http transport listens on loopback
unless
--hostsays otherwise, and has no authentication either.
One Server per Agent
The engine tells clients apart by their RoIS session. It gives an actuation component (one that moves, speaks, or shows) to one session at a time, and sends a command's completion to the session that ran it. Each server process holds one session.
- With stdio, every agent starts its own server, so every agent has its own session. Two agents
that want the same robot then take turns through
bind. - Agents that share one server share one session. This includes subagents inside one client. A bind by one of them covers the others, and the engine cannot tell them apart.
- The engine does not say which session holds a component. An agent learns that it is busy,
not who has it. The hint of a busy component tells the agent to tell the user and to ask
before it waits for the component with
bind(wait_seconds).
Tools
| Tool | What it does |
|---|---|
list_components(pattern?) |
Every component with its type, engine, function, commands, queries and events, its component_status for this session (BUSY when another session holds it), held_by_this_session and actions_allowed |
describe_component(ref) |
The commands, queries, events and parameters of one component, with their types, argument defaults, read-only flags, each parameter's initial_value, and current values when bound |
query(ref, query_type) |
Runs a query and returns its results as JSON values |
get_parameters(ref) |
The current parameters. An actuation component must be bound first. Another component is bound for the read, only by a server that may act on it |
subscribe(ref, event_type), unsubscribe(subscribe_id) |
Follow an event of one component. For a component with a start command, the reply says it may send events only once started |
get_events(since?, limit?) |
The notifications after a cursor, and the next cursor |
wait_for_event(event_type?, ref?, timeout?, since?) |
Waits for a matching event, an error, a loss, or the timeout. Without since, it starts at the cursor of the subscribe, run_commands or set_parameters just before it, or else at the newest entry |
get_error_detail(error_id) |
The details of an error entry |
command_status(command_ids) |
How commands of this session ended, with their results |
bind(ref, wait_seconds?) |
Reserves a component. With wait_seconds, retries while another session holds it |
release(ref) |
Frees a component, and names the commands of this session that ended ABORT with it |
set_parameters(ref, values, wait?) |
Sets parameters from plain JSON values and waits until they apply |
run_commands(steps, wait?, timeout?, auto_bind?) |
Runs commands and parallel groups, and returns how each one ended |
The last four exist only with --allow-actions.
get_events returns entries of these kinds: event, completed (a command ended), error,
profile_changed (components joined or left), lost (binds and subscriptions the engine
dropped), connection_closed and idle_release.
Waiting for Events
subscribe, run_commands, set_parameters, get_events and wait_for_event each return a
cursor. The first wait_for_event without since after subscribe, run_commands or
set_parameters starts at the cursor that call returned. So an event a component sends while
a command runs, before the command completes, still reaches the wait that follows, as in
subscribe, then run_commands with a person detector's start, then
wait_for_event("person_detected"). Any other wait without since starts at the newest entry,
so asking again later waits for a fresh count instead of returning an old one. To read every
entry in turn, pass the cursor of the last wait as since. A wait that times out returns the
cursor it started at.
Some components, such as a person detector, send events only between their start and
stop commands. A read-only server cannot start them.
Running Commands
{
"steps": [
{"component_ref": "mock/navigation", "command_type": "start"},
{
"branch_list": [
[{"component_ref": "robot/speech", "command_type": "speak", "arguments": {"text": "Hi"}}],
[{"component_ref": "robot/head", "command_type": "nod"}]
],
"delay_time": 500
}
]
}
A step is a command, or a parallel group whose branches run at the same time, each branch in
order, after delay_time milliseconds. Arguments are plain JSON values: the server types them
from the component profile. With wait (the default), the call returns when every command has
ended or the timeout passes, with each status: OK, ERROR, ABORT, TIMEOUT,
OUT_OF_RESOURCES, RUNNING for a command that had not ended yet, or CONNECTION_LOST when
the gateway connection went during the wait.
A command that did not end OK comes with a hint. One that ended ERROR also carries the
component's component_status and the error entries its engine sent after the command
started. One that ended ABORT after a failed command names it.
Many commands, such as the start of a reaction or of speech, take no arguments and read the
component's parameters instead. A set_parameter step sets them in the same call, right
before the command that reads them:
{
"steps": [
{"component_ref": "mock/navigation", "command_type": "set_parameter",
"arguments": {"target_positions": ["kitchen"]}},
{"component_ref": "mock/navigation", "command_type": "start"}
]
}
Parameters stay on the component between sessions, so set every parameter a command reads
before each start. initial_value in describe_component is only the value before anyone set
it.
Every component must be bound by this session first. Otherwise the call fails before anything is sent:
{"step": "bind", "component_ref": "mock/navigation", "return_code": "NOT_BOUND",
"hint": "not bound by this session, call bind first"}
With auto_bind, the server binds the components itself, all or nothing: if a bind fails, it
releases the binds this call made, runs nothing, and names the component and the code, for
example OUT_OF_RESOURCES with the hint that another session holds it. A component this
session holds is bound again too, which renews a bind the engine dropped. Binds stay until
release, the end of the session, or the idle release.
Resources
| URI | Content |
|---|---|
rois://components |
Every component with its type and what it offers |
rois://components/{ref} |
One component profile, condensed like describe_component |
rois://session |
This session's binds, subscriptions, running commands and connection |
Errors
A RoIS call that does not return OK becomes a tool error that names the method, the return
code and what to do, such as rois.query.query returned UNSUPPORTED for mock/navigation: the component does not declare this, see describe_component. The hint of ERROR depends on the
call: for a query or a subscribe, the component cannot serve now, for example because its
device is unreachable. For a command operation, the session holds no bind, or the engine
dropped it. A value the component refused names the queries that list valid values, such as
available_reactions.
When the engine of a component leaves the gateway or reconnects to it, the engine drops the
binds and subscriptions of every session on its components. The server notices, adds a lost
entry that names them, and says so in the notice of the next tool response. bind always
asks the engine, so binding again works.
When the gateway connection drops, the server does not reconnect in the middle of a call. A
wait on commands ends at once with CONNECTION_LOST. The next tool response, even a failed
one, says in its notice that the binds and subscriptions of the old session are gone, and
the next call opens a new session.
Configuration
Each flag has an environment variable, and a flag overrides its variable.
| Flag | Variable | Default | Meaning |
|---|---|---|---|
--gateway-url |
OPENROIS_MCP_GATEWAY_URL |
ws://127.0.0.1:8765 |
The gateway |
--allow-actions |
OPENROIS_MCP_ALLOW_ACTIONS |
off | Offer the tools that act |
--allow PATTERN |
OPENROIS_MCP_ALLOW (comma-separated) |
every component | Components an agent may act on |
--command-timeout |
OPENROIS_MCP_COMMAND_TIMEOUT |
60 | The longest wait for a command, in seconds |
--event-buffer |
OPENROIS_MCP_EVENT_BUFFER |
500 | Notifications kept for get_events |
--idle-release SECONDS |
OPENROIS_MCP_IDLE_RELEASE |
off | Release a bind with no call on its component for this long |
--request-timeout |
OPENROIS_MCP_REQUEST_TIMEOUT |
30 | The wait for one RoIS reply, in seconds |
--transport |
OPENROIS_MCP_TRANSPORT |
stdio |
stdio or streamable-http |
--host, --port |
OPENROIS_MCP_HOST, OPENROIS_MCP_PORT |
127.0.0.1, 8000 |
Where streamable-http listens |
Not in This Version
- One tool per component command, generated from the profiles.
- Authentication and several users on one streamable-http server, which wait for authentication at the gateway.
- Audio and video streams, which wait for media in the SDK.
- Several gateways from one server.
Tests
pip install -e ../../interfaces/python -e ../../sdk/python -e ".[dev]"
pytest # unit tests
OPENROIS_GATEWAY_URL=ws://127.0.0.1:8765 pytest # also against the compose stack
mypy
ruff check src/ tests/
License
Apache-2.0. See LICENSE.
Metadata
Release files for openrois-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| openrois_mcp-0.1.0.tar.gz | 50.2 kB | With provenance | Details for openrois_mcp-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| openrois_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for openrois_mcp-0.1.0-py3-none-any.whl |
Total release size: 89.0 kB
Release files / openrois_mcp-0.1.0.tar.gz
| Download URL | openrois_mcp-0.1.0.tar.gz |
|---|---|
| Size | 50.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a2c56afcdda6b818eac4234bbc8450d8efe12e4a39df3975147f157c33d76f77
|
|
BLAKE2b-256 checksum How to use checksums |
ee30957b3b46130e46a82d92b29f4a088089877c7e51aca09a83314cb9f4315d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logRelease files / openrois_mcp-0.1.0-py3-none-any.whl
| Download URL | openrois_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 38.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e31e5196ae02f90646943b84d7401a0c0f28da81c681a5655320dd94809b7eda
|
|
BLAKE2b-256 checksum How to use checksums |
6962e70f6c2ddc960cfd17b62a9fde52eb8fc1b3a1cc4a3e0cd1e7dab5d49ea6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logSecurity for openrois-mcp 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for openrois_mcp-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.