Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 2 additions & 98 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,70 +212,9 @@ jobs:
uses: ./.github/workflows/tests.yml
secrets: inherit
with:
cmd: "pytest && pytest -m ros" # run tests that depend on ros as well
cmd: "pytest --durations=0 -m 'not (tool or mujoco)'"
dev-image: ros-dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true' || needs.check-changes.outputs.ros == 'true') && needs.ros-dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }}

run-tests:
needs: [check-changes, dev]
if: ${{
always() &&
needs.check-changes.result == 'success' &&
(needs.check-changes.outputs.tests == 'true' ||
needs.check-changes.outputs.python == 'true' ||
needs.check-changes.outputs.dev == 'true')
}}
uses: ./.github/workflows/tests.yml
secrets: inherit
with:
cmd: "pytest"
dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }}

# we run in parallel with normal tests for speed
run-heavy-tests:
needs: [check-changes, dev]
if: ${{
always() &&
needs.check-changes.result == 'success' &&
(needs.check-changes.outputs.tests == 'true' ||
needs.check-changes.outputs.python == 'true' ||
needs.check-changes.outputs.dev == 'true')
}}
uses: ./.github/workflows/tests.yml
secrets: inherit
with:
cmd: "pytest -m heavy"
dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }}

run-lcm-tests:
needs: [check-changes, dev]
if: ${{
always() &&
needs.check-changes.result == 'success' &&
(needs.check-changes.outputs.tests == 'true' ||
needs.check-changes.outputs.python == 'true' ||
needs.check-changes.outputs.dev == 'true')
}}
uses: ./.github/workflows/tests.yml
secrets: inherit
with:
cmd: "pytest -m lcm"
dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }}

run-integration-tests:
needs: [check-changes, dev]
if: ${{
always() &&
needs.check-changes.result == 'success' &&
(needs.check-changes.outputs.tests == 'true' ||
needs.check-changes.outputs.python == 'true' ||
needs.check-changes.outputs.dev == 'true')
}}
uses: ./.github/workflows/tests.yml
secrets: inherit
with:
cmd: "pytest -m integration"
dev-image: dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true') && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }}

run-mypy:
needs: [check-changes, ros-dev]
if: ${{
Expand All @@ -292,43 +231,8 @@ jobs:
cmd: "MYPYPATH=/opt/ros/humble/lib/python3.10/site-packages mypy dimos"
dev-image: ros-dev:${{ (needs.check-changes.outputs.python == 'true' || needs.check-changes.outputs.dev == 'true' || needs.check-changes.outputs.ros == 'true') && needs.ros-dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }}

# Run module tests directly to avoid pytest forking issues
# run-module-tests:
# needs: [check-changes, dev]
# if: ${{
# always() &&
# needs.check-changes.result == 'success' &&
# ((needs.dev.result == 'success') ||
# (needs.dev.result == 'skipped' &&
# needs.check-changes.outputs.tests == 'true'))
# }}
# runs-on: [self-hosted, x64, 16gb]
# container:
# image: ghcr.io/dimensionalos/dev:${{ needs.check-changes.outputs.dev == 'true' && needs.dev.result == 'success' && needs.check-changes.outputs.branch-tag || 'dev' }}
# steps:
# - name: Fix permissions
# run: |
# sudo chown -R $USER:$USER ${{ github.workspace }} || true
#
# - uses: actions/checkout@v4
# with:
# lfs: true
#
# - name: Configure Git LFS
# run: |
# git config --global --add safe.directory '*'
# git lfs install
# git lfs fetch
# git lfs checkout
#
# - name: Run module tests
# env:
# CI: "true"
# run: |
# /entrypoint.sh bash -c "pytest -m module"

ci-complete:
needs: [check-changes, ros, python, ros-python, dev, ros-dev, run-tests, run-heavy-tests, run-lcm-tests, run-integration-tests, run-ros-tests, run-mypy]
needs: [check-changes, ros, python, ros-python, dev, ros-dev, run-ros-tests, run-mypy]
runs-on: [self-hosted, Linux]
if: always()
steps:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,5 @@ yolo11n.pt

CLAUDE.MD
/assets/teleop_certs/

/.mcp.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ Dimensional is agent native -- "vibecode" your robots in natural language and bu
🟥 <a href="dimos/robot/unitree/b1">Unitree B1</a><br>
</td>
<td align="center" width="20%">
🟨 <a href="docs/todo.md">Unitree G1</a><br>
🟨 <a href="docs/platforms/humanoid/g1/index.md">Unitree G1</a><br>
</td>
<td align="center" width="20%">
🟥 <a href="docs/todo.md">Xarm</a><br>
Expand Down
32 changes: 32 additions & 0 deletions bin/gen-diagrams
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -euo pipefail

REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT"

# if md-babel-py doesnt exist
if [ -z "$(command -v "md-babel-py")" ]; then
# if nix doesnt exist
if [ -z "$(command -v "nix")" ]; then
echo "Error: md-babel-py required for running gen-diagrams." >&2
echo " Either install nix or install md-babel-py" >&2
echo " https://github.com/leshy/md-babel-py" >&2
exit 1
# use nix if local command doesn't exist
else
md-babel-py() {
nix run github:leshy/md-babel-py -- "$@"
}
fi
fi

diagram_langs="asymptote,pikchr,openscad,diagon"
if [[ "$#" -gt 0 ]]; then
for arg in "$@"; do
md-babel-py run "$arg" --lang "$diagram_langs"
done
else
while IFS= read -r file; do
md-babel-py run "$file" --lang "$diagram_langs"
done < <(find ./docs -type f -name '*.md' | sort)
fi
2 changes: 1 addition & 1 deletion bin/pytest-slow
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
set -euo pipefail

. .venv/bin/activate
exec pytest "$@" -m 'not (tool or module or neverending or mujoco)' dimos
exec pytest "$@" -m 'not (tool or mujoco)' dimos
5 changes: 2 additions & 3 deletions dimos/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,8 @@ class AgentConfig(ModuleConfig):
model_fixture: str | None = None


class Agent(Module):
default_config: type[AgentConfig] = AgentConfig
config: AgentConfig
class Agent(Module[AgentConfig]):
default_config = AgentConfig
agent: Out[BaseMessage]
human_input: In[str]
agent_idle: Out[bool]
Expand Down
5 changes: 0 additions & 5 deletions dimos/agents/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,6 @@
FIXTURE_DIR = Path(__file__).parent / "fixtures"


@pytest.fixture
def fixture_dir() -> Path:
return FIXTURE_DIR


@pytest.fixture
def agent_setup(request):
coordinator = None
Expand Down
55 changes: 55 additions & 0 deletions dimos/agents/mcp/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# DimOS MCP Server

Expose DimOS robot skills to Claude Code via Model Context Protocol.

## Setup

```bash
uv sync --extra base --extra unitree
```

Add to Claude Code (one command)

```bash
claude mcp add --transport http --scope project dimos http://localhost:9990/mcp
```

Verify that it was added:

```bash
claude mcp list
```

## MCP Inspector

If you want to inspect the server manually, you can use MCP Inspector.

Install it:

```bash
npx -y @modelcontextprotocol/inspector
```

It will open a browser window.

Change **Transport Type** to "Streamable HTTP", change **URL** to `http://localhost:9990/mcp`, and **Connection Type** to "Direct". Then click on "Connect".

## Usage

**Terminal 1** - Start DimOS:
```bash
uv run dimos run unitree-go2-agentic-mcp
```

**Claude Code** - Use robot skills:
```
> move forward 1 meter
> go to the kitchen
> tag this location as "desk"
```

## How It Works

1. `McpServer` in the blueprint starts a FastAPI server on port 9990
2. Claude Code connects directly to `http://localhost:9990/mcp`
3. Skills are exposed as MCP tools (e.g., `relative_move`, `navigate_with_text`)
File renamed without changes.
103 changes: 103 additions & 0 deletions dimos/agents/mcp/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright 2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import os
from pathlib import Path
from threading import Event

from dotenv import load_dotenv
from langchain_core.messages.base import BaseMessage
import pytest

from dimos.agents.agent_test_runner import AgentTestRunner
from dimos.agents.mcp.mcp_client import McpClient
from dimos.agents.mcp.mcp_server import McpServer
from dimos.core.blueprints import autoconnect
from dimos.core.global_config import global_config
from dimos.core.transport import pLCMTransport

load_dotenv()

FIXTURE_DIR = Path(__file__).parent / "fixtures"


@pytest.fixture
def agent_setup(request):
coordinator = None
transports: list[pLCMTransport] = []
unsubs: list = []
recording = bool(os.getenv("RECORD"))

def fn(
*,
blueprints,
messages: list[BaseMessage],
dask: bool = False,
system_prompt: str | None = None,
fixture: str | None = None,
) -> list[BaseMessage]:
history: list[BaseMessage] = []
finished_event = Event()

agent_transport: pLCMTransport = pLCMTransport("/agent")
finished_transport: pLCMTransport = pLCMTransport("/finished")
transports.extend([agent_transport, finished_transport])

def on_message(msg: BaseMessage) -> None:
history.append(msg)

unsubs.append(agent_transport.subscribe(on_message))
unsubs.append(finished_transport.subscribe(lambda _: finished_event.set()))

# Derive fixture path from test name if not explicitly provided.
if fixture is not None:
fixture_path = FIXTURE_DIR / fixture
else:
fixture_path = FIXTURE_DIR / f"{request.node.name}.json"

client_kwargs: dict = {"system_prompt": system_prompt}

if recording or fixture_path.exists():
client_kwargs["model_fixture"] = str(fixture_path)

blueprint = autoconnect(
*blueprints,
McpServer.blueprint(),
McpClient.blueprint(**client_kwargs),
AgentTestRunner.blueprint(messages=messages),
)

global_config.update(
viewer_backend="none",
dask=dask,
)

nonlocal coordinator
coordinator = blueprint.build()

if not finished_event.wait(60):
raise TimeoutError("Timed out waiting for agent to finish processing messages.")

return history

yield fn

if coordinator is not None:
coordinator.stop()

for transport in transports:
transport.stop()

for unsub in unsubs:
unsub()
34 changes: 34 additions & 0 deletions dimos/agents/mcp/fixtures/test_can_call_again_on_error[False].json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"responses": [
{
"content": "",
"tool_calls": [
{
"name": "register_user",
"args": {
"name": "Paul"
},
"id": "call_NrrizXSIFaeCLuG9i05IwDy3",
"type": "tool_call"
}
]
},
{
"content": "",
"tool_calls": [
{
"name": "register_user",
"args": {
"name": "paul"
},
"id": "call_2QPx4GsL61Xjrggbq7afXTjn",
"type": "tool_call"
}
]
},
{
"content": "The user named 'paul' has been registered successfully.",
"tool_calls": []
}
]
}
Loading
Loading