Skip to content

Feat/mcp server expose#2539

Open
lianghaofeng wants to merge 3 commits intoOpenMind:mainfrom
lianghaofeng:feat/mcp-server-expose
Open

Feat/mcp server expose#2539
lianghaofeng wants to merge 3 commits intoOpenMind:mainfrom
lianghaofeng:feat/mcp-server-expose

Conversation

@lianghaofeng
Copy link
Copy Markdown

Problem

OM1 currently consumes MCP servers (src/mcp_servers/client.py) but cannot expose itself as one. External MCP clients (Claude Desktop, Cursor, Windsurf) have no standard way to drive the OM1 virtual agent.

Solution

Add src/expose/ — a stdio MCP server exposing three tools:

Tool Schema
om1_move enum: forward, backward, turn_left, turn_right, spin, sit, stand, idle
om1_speak string, 1–500 chars
om1_face enum: joy, smile, ponder, alert, sad

All tools use JSON Schema enum constraints so the LLM sees a precise tool contract instead of free-form strings (validated end-to-end with Claude Desktop — "moonwalk" correctly rejected, LLM falls back to spin).

Architecture

  • server.py — MCP wiring + pure handle_tool_call() dispatcher
  • websim_adapter.py — WebSim interop with fail-fast port conflict check
  • tools.py — Enum definitions and tool schemas
  • config.py — Env-var config (OM1_WEBSIM_HOST/PORT, OM1_LOG_LEVEL)

Split this way so tests can inject a MagicMock adapter without starting a real WebSim HTTP server.

Usage

uv pip install -e .
om1-mcp-server

Claude Desktop config becomes one line:

{
  "mcpServers": {
    "om1": {
      "command": "/absolute/path/.venv/bin/om1-mcp-server"
    }
  }
}

Verification

  • 24 unit tests: uv run pytest tests/expose/ — all green
  • No regression on 263 core tests
  • pre-commit run on staged files — clean
  • End-to-end tested with Claude Desktop + WebSim — tools invoke correctly, enum constraint rejects invalid actions

Known Limitation (follow-up candidate)

WebSim.py:509-510 hardcodes host="0.0.0.0" and port=8000, ignoring the SimulatorConfig passed to it. OM1_WEBSIM_PORT is plumbed through correctly on the expose side but won't take effect until WebSim reads its own config. Happy to submit a follow-up PR for that if the team is open to it.

Out of Scope

  • Additional simulators beyond WebSim
  • Streaming tool responses
  • Multi-agent fleet exposure

Per CONTRIBUTING.md, I skipped opening an issue first because the change is additive, non-invasive (no existing file touched except pyproject.toml to register the CLI), and fully tested. If a discussion thread is preferred, happy to convert this into a draft.

Add src/expose/ — a stdio MCP server that exposes OM1 tools to
external MCP clients (Claude Desktop, Cursor, Windsurf).

Tools:
  - om1_move: 8 actions (forward/backward/turn_left/turn_right/spin/sit/stand/idle)
  - om1_speak: text (1-500 chars)
  - om1_face: 5 emotions (joy/smile/ponder/alert/sad)

All tools validated via JSON Schema enums, giving the LLM a precise
tool contract instead of free-form strings.

Architecture:
  - server.py: MCP wiring and pure handle_tool_call() dispatcher
  - websim_adapter.py: WebSim interop with fail-fast port conflict check
  - tools.py: Enum definitions and tool schemas
  - config.py: Env-var config (OM1_WEBSIM_HOST/PORT, OM1_LOG_LEVEL)

Known limitation: WebSim.py hardcodes port 8000 in _run_server (L509).
OM1_WEBSIM_PORT is plumbed correctly on our side but won't take effect
until WebSim reads its SimulatorConfig. Follow-up PR candidate.
24 tests covering:
  - tools: schema structure, enum constraints on om1_move/om1_face,
    om1_speak text length bounds
  - config: env-var loading and defaults, ValueError on invalid port
  - websim_adapter: move/speak/face dispatch via MagicMock, real
    socket check for ensure_port_free (pass + raise paths)
  - server: handle_tool_call 8 paths — 3 happy paths, enum rejection,
    empty-text rejection, unknown tool, KeyError fallback

All tests use real code (no mocking of our own modules); only the
WebSim boundary is mocked via adapter injection.
Declare project.scripts so users can launch the server with a single
command after `uv pip install -e .`:

  om1-mcp-server

This simplifies Claude Desktop / Cursor configuration from multi-line
(with explicit python path, PYTHONPATH, cwd, and -m module) to a
single 'command' field pointing at the generated binary.
@lianghaofeng lianghaofeng requested review from a team as code owners April 18, 2026 16:14
@github-actions github-actions Bot added dependencies Pull requests that update a dependency file robotics Robotics code changes python Python code tests Test files config Configuration files labels Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

config Configuration files dependencies Pull requests that update a dependency file python Python code robotics Robotics code changes tests Test files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant