Skip to content
Merged
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
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
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": []
}
]
}
34 changes: 34 additions & 0 deletions dimos/agents/mcp/fixtures/test_can_call_again_on_error[True].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_XSy1Dx1dGtQv5zPaEJtb2hd7",
"type": "tool_call"
}
]
},
{
"content": "",
"tool_calls": [
{
"name": "register_user",
"args": {
"name": "paul"
},
"id": "call_aYFug1g3TATnaYus9HUVxoQS",
"type": "tool_call"
}
]
},
{
"content": "The user named \"paul\" has been registered successfully.",
"tool_calls": []
}
]
}
22 changes: 22 additions & 0 deletions dimos/agents/mcp/fixtures/test_can_call_tool[False].json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"responses": [
{
"content": "",
"tool_calls": [
{
"name": "add",
"args": {
"x": 33333,
"y": 100
},
"id": "call_RssRDDd9apDjNoVLz4jRLVk0",
"type": "tool_call"
}
]
},
{
"content": "The result of 33333 + 100 is 33433.",
"tool_calls": []
}
]
}
22 changes: 22 additions & 0 deletions dimos/agents/mcp/fixtures/test_can_call_tool[True].json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"responses": [
{
"content": "",
"tool_calls": [
{
"name": "add",
"args": {
"x": 33333,
"y": 100
},
"id": "call_pzzddF9mBynGYZVdCmGHOB5V",
"type": "tool_call"
}
]
},
{
"content": "The result of 33333 + 100 is 33433.",
"tool_calls": []
}
]
}
23 changes: 23 additions & 0 deletions dimos/agents/mcp/fixtures/test_image.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"responses": [
{
"content": "",
"tool_calls": [
{
"name": "take_a_picture",
"args": {},
"id": "call_7Qwsr8QMLWhKRMektcGiKYf7",
"type": "tool_call"
}
]
},
{
"content": "I've taken a picture. Let me analyze and describe it for you.\nThe image features an expansive outdoor stadium. From the camera's perspective, the word 'stadium' best matches the image. Is there anything else you'd like to know or do?",
"tool_calls": []
},
{
"content": "The image shows a group of people sitting and enjoying their time at an outdoor cafe. Therefore, the word 'cafe' best matches the image.",
"tool_calls": []
}
]
}
Loading
Loading