Skip to content

Commit e7594ae

Browse files
feat(mcp): add mcp client and server (#1300)
* Moving towards deprecating `Agent`. It has been efectivelly split between `McpClient` and `McpServer`. * McpServer exposes all the `@skill` functions as MCP tools. * McpClient starts a langchain agent which coordinates calling tools from McpServer. * McpClient is not strictly necessary. You can register the MCP server in claude code and make it call tools. * McpClient also listens to `/human_input` so it can be used through the `humancli` like `Agent` can.
1 parent b150b32 commit e7594ae

26 files changed

+1260
-375
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,5 @@ yolo11n.pt
6969

7070
CLAUDE.MD
7171
/assets/teleop_certs/
72+
73+
/.mcp.json

dimos/agents/agent.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,8 @@ class AgentConfig(ModuleConfig):
4646
model_fixture: str | None = None
4747

4848

49-
class Agent(Module):
50-
default_config: type[AgentConfig] = AgentConfig
51-
config: AgentConfig
49+
class Agent(Module[AgentConfig]):
50+
default_config = AgentConfig
5251
agent: Out[BaseMessage]
5352
human_input: In[str]
5453
agent_idle: Out[bool]

dimos/agents/conftest.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,6 @@
3131
FIXTURE_DIR = Path(__file__).parent / "fixtures"
3232

3333

34-
@pytest.fixture
35-
def fixture_dir() -> Path:
36-
return FIXTURE_DIR
37-
38-
3934
@pytest.fixture
4035
def agent_setup(request):
4136
coordinator = None

dimos/agents/mcp/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# DimOS MCP Server
2+
3+
Expose DimOS robot skills to Claude Code via Model Context Protocol.
4+
5+
## Setup
6+
7+
```bash
8+
uv sync --extra base --extra unitree
9+
```
10+
11+
Add to Claude Code (one command)
12+
13+
```bash
14+
claude mcp add --transport http --scope project dimos http://localhost:9990/mcp
15+
```
16+
17+
Verify that it was added:
18+
19+
```bash
20+
claude mcp list
21+
```
22+
23+
## MCP Inspector
24+
25+
If you want to inspect the server manually, you can use MCP Inspector.
26+
27+
Install it:
28+
29+
```bash
30+
npx -y @modelcontextprotocol/inspector
31+
```
32+
33+
It will open a browser window.
34+
35+
Change **Transport Type** to "Streamable HTTP", change **URL** to `http://localhost:9990/mcp`, and **Connection Type** to "Direct". Then click on "Connect".
36+
37+
## Usage
38+
39+
**Terminal 1** - Start DimOS:
40+
```bash
41+
uv run dimos run unitree-go2-agentic-mcp
42+
```
43+
44+
**Claude Code** - Use robot skills:
45+
```
46+
> move forward 1 meter
47+
> go to the kitchen
48+
> tag this location as "desk"
49+
```
50+
51+
## How It Works
52+
53+
1. `McpServer` in the blueprint starts a FastAPI server on port 9990
54+
2. Claude Code connects directly to `http://localhost:9990/mcp`
55+
3. Skills are exposed as MCP tools (e.g., `relative_move`, `navigate_with_text`)

dimos/agents/mcp/conftest.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright 2026 Dimensional Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
from pathlib import Path
17+
from threading import Event
18+
19+
from dotenv import load_dotenv
20+
from langchain_core.messages.base import BaseMessage
21+
import pytest
22+
23+
from dimos.agents.agent_test_runner import AgentTestRunner
24+
from dimos.agents.mcp.mcp_client import McpClient
25+
from dimos.agents.mcp.mcp_server import McpServer
26+
from dimos.core.blueprints import autoconnect
27+
from dimos.core.global_config import global_config
28+
from dimos.core.transport import pLCMTransport
29+
30+
load_dotenv()
31+
32+
FIXTURE_DIR = Path(__file__).parent / "fixtures"
33+
34+
35+
@pytest.fixture
36+
def agent_setup(request):
37+
coordinator = None
38+
transports: list[pLCMTransport] = []
39+
unsubs: list = []
40+
recording = bool(os.getenv("RECORD"))
41+
42+
def fn(
43+
*,
44+
blueprints,
45+
messages: list[BaseMessage],
46+
dask: bool = False,
47+
system_prompt: str | None = None,
48+
fixture: str | None = None,
49+
) -> list[BaseMessage]:
50+
history: list[BaseMessage] = []
51+
finished_event = Event()
52+
53+
agent_transport: pLCMTransport = pLCMTransport("/agent")
54+
finished_transport: pLCMTransport = pLCMTransport("/finished")
55+
transports.extend([agent_transport, finished_transport])
56+
57+
def on_message(msg: BaseMessage) -> None:
58+
history.append(msg)
59+
60+
unsubs.append(agent_transport.subscribe(on_message))
61+
unsubs.append(finished_transport.subscribe(lambda _: finished_event.set()))
62+
63+
# Derive fixture path from test name if not explicitly provided.
64+
if fixture is not None:
65+
fixture_path = FIXTURE_DIR / fixture
66+
else:
67+
fixture_path = FIXTURE_DIR / f"{request.node.name}.json"
68+
69+
client_kwargs: dict = {"system_prompt": system_prompt}
70+
71+
if recording or fixture_path.exists():
72+
client_kwargs["model_fixture"] = str(fixture_path)
73+
74+
blueprint = autoconnect(
75+
*blueprints,
76+
McpServer.blueprint(),
77+
McpClient.blueprint(**client_kwargs),
78+
AgentTestRunner.blueprint(messages=messages),
79+
)
80+
81+
global_config.update(
82+
viewer_backend="none",
83+
dask=dask,
84+
)
85+
86+
nonlocal coordinator
87+
coordinator = blueprint.build()
88+
89+
if not finished_event.wait(60):
90+
raise TimeoutError("Timed out waiting for agent to finish processing messages.")
91+
92+
return history
93+
94+
yield fn
95+
96+
if coordinator is not None:
97+
coordinator.stop()
98+
99+
for transport in transports:
100+
transport.stop()
101+
102+
for unsub in unsubs:
103+
unsub()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"responses": [
3+
{
4+
"content": "",
5+
"tool_calls": [
6+
{
7+
"name": "register_user",
8+
"args": {
9+
"name": "Paul"
10+
},
11+
"id": "call_NrrizXSIFaeCLuG9i05IwDy3",
12+
"type": "tool_call"
13+
}
14+
]
15+
},
16+
{
17+
"content": "",
18+
"tool_calls": [
19+
{
20+
"name": "register_user",
21+
"args": {
22+
"name": "paul"
23+
},
24+
"id": "call_2QPx4GsL61Xjrggbq7afXTjn",
25+
"type": "tool_call"
26+
}
27+
]
28+
},
29+
{
30+
"content": "The user named 'paul' has been registered successfully.",
31+
"tool_calls": []
32+
}
33+
]
34+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
{
2+
"responses": [
3+
{
4+
"content": "",
5+
"tool_calls": [
6+
{
7+
"name": "register_user",
8+
"args": {
9+
"name": "Paul"
10+
},
11+
"id": "call_XSy1Dx1dGtQv5zPaEJtb2hd7",
12+
"type": "tool_call"
13+
}
14+
]
15+
},
16+
{
17+
"content": "",
18+
"tool_calls": [
19+
{
20+
"name": "register_user",
21+
"args": {
22+
"name": "paul"
23+
},
24+
"id": "call_aYFug1g3TATnaYus9HUVxoQS",
25+
"type": "tool_call"
26+
}
27+
]
28+
},
29+
{
30+
"content": "The user named \"paul\" has been registered successfully.",
31+
"tool_calls": []
32+
}
33+
]
34+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"responses": [
3+
{
4+
"content": "",
5+
"tool_calls": [
6+
{
7+
"name": "add",
8+
"args": {
9+
"x": 33333,
10+
"y": 100
11+
},
12+
"id": "call_RssRDDd9apDjNoVLz4jRLVk0",
13+
"type": "tool_call"
14+
}
15+
]
16+
},
17+
{
18+
"content": "The result of 33333 + 100 is 33433.",
19+
"tool_calls": []
20+
}
21+
]
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"responses": [
3+
{
4+
"content": "",
5+
"tool_calls": [
6+
{
7+
"name": "add",
8+
"args": {
9+
"x": 33333,
10+
"y": 100
11+
},
12+
"id": "call_pzzddF9mBynGYZVdCmGHOB5V",
13+
"type": "tool_call"
14+
}
15+
]
16+
},
17+
{
18+
"content": "The result of 33333 + 100 is 33433.",
19+
"tool_calls": []
20+
}
21+
]
22+
}

0 commit comments

Comments
 (0)