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
73 changes: 73 additions & 0 deletions .github/workflows/auto-version.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
name: Auto Versioning

on:
push:
branches:
- master
paths-ignore:
- 'README.md'
- 'pyproject.toml'

jobs:
version:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, 'chore: bump version')"
permissions:
contents: write

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Increment version
id: bump
run: |
python -c "
import re
from pathlib import Path

# Update pyproject.toml
pyproject = Path('pyproject.toml')
content = pyproject.read_text()
version_match = re.search(r'version = \"(\d+)\.(\d+)\.(\d+)\"', content)
if not version_match:
raise ValueError('Could not find version in pyproject.toml')

major, minor, patch = map(int, version_match.groups())
new_patch = patch + 1
new_version = f'{major}.{minor}.{new_patch}'

new_content = re.sub(r'version = \".*\"', f'version = \"{new_version}\"', content, count=1)
pyproject.write_text(new_content)

# Update README.md badge
readme = Path('README.md')
readme_content = readme.read_text()
new_readme = re.sub(r'badge/version-.*-blue', f'badge/version-{new_version}-blue', readme_content)
readme.write_text(new_readme)

import os
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f'new_version={new_version}\n')
"

- name: Commit and push changes
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add pyproject.toml README.md
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }}"
git push origin master

- name: Create Tag
run: |
git tag v${{ steps.bump.outputs.new_version }}
git push origin v${{ steps.bump.outputs.new_version }}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Natural language robot control using **FastMCP** and **Multi-LLM Support** (OpenAI, Groq, Gemini, Ollama).

![Version](https://img.shields.io/badge/version-0.3.0-blue)
[![Python](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![codecov](https://codecov.io/gh/dgaida/robot_mcp/branch/master/graph/badge.svg)](https://codecov.io/gh/dgaida/robot_mcp)
Expand Down
10 changes: 10 additions & 0 deletions client/fastmcp_groq_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,16 @@ async def connect(self):
self.available_tools = await self.client.list_tools()
print(f"Connected! Found tools: {[t.name for t in self.available_tools]}")

# Call get_system_status immediately after connection
tool_names = [t.name for t in self.available_tools]
if "get_system_status" in tool_names:
print("\n📊 Initial System Status:")
status = await self.call_tool("get_system_status", {})
# Store initial status in conversation history for LLM context
self.conversation_history.append(
{"role": "assistant", "content": f"I have initialized the system. Current status: {status}"}
)

async def disconnect(self):
if hasattr(self, "client"):
await self.client.__aexit__(None, None, None)
Expand Down
9 changes: 9 additions & 0 deletions client/fastmcp_universal_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,15 @@ async def connect(self):
print(f"Model: {self.llm_client.llm}")
print(f"Found {len(self.available_tools)} tools: {tool_names}")

# Call get_system_status immediately after connection
if "get_system_status" in tool_names:
print("\n📊 Initial System Status:")
status = await self.call_tool("get_system_status", {})
# Store initial status in conversation history for LLM context
self.conversation_history.append(
{"role": "assistant", "content": f"I have initialized the system. Current status: {status}"}
)

async def disconnect(self):
"""Disconnect from MCP server."""
logger.info("=" * 60)
Expand Down
9 changes: 9 additions & 0 deletions client/fastmcp_universal_client_with_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,15 @@ async def connect(self):
print(f" Model: {self.llm_client.llm}")
print(f" Found {len(self.available_tools)} tools: {tool_names}")

# Call get_system_status immediately after connection
if "get_system_status" in tool_names:
print("\n📊 Initial System Status:")
status = await self.call_tool("get_system_status", {})
# Store initial status in conversation history for LLM context
self.conversation_history.append(
{"role": "assistant", "content": f"I have initialized the system. Current status: {status}"}
)

async def disconnect(self):
"""Disconnect from MCP server."""
self.logger.info("=" * 60)
Expand Down
38 changes: 37 additions & 1 deletion config/config_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from typing import Any, Dict, Optional

import yaml
from pydantic import BaseModel, Field, ValidationError, field_validator
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator

# ============================================================================
# PYDANTIC MODELS FOR TYPE-SAFE CONFIGURATION
Expand All @@ -38,6 +38,8 @@
class ServerConfig(BaseModel):
"""Server configuration settings."""

model_config = ConfigDict(extra="forbid")

host: str = Field("127.0.0.1", description="Server host address")
port: int = Field(8000, ge=1024, le=65535, description="Server port")
max_workers: int = Field(4, ge=1, description="Maximum worker threads")
Expand All @@ -56,6 +58,8 @@ def validate_log_level(cls, v: str) -> str:
class WorkspaceBounds(BaseModel):
"""Workspace boundary coordinates."""

model_config = ConfigDict(extra="forbid")

x_min: float = Field(..., description="Minimum X coordinate (meters)")
x_max: float = Field(..., description="Maximum X coordinate (meters)")
y_min: float = Field(..., description="Minimum Y coordinate (meters)")
Expand All @@ -79,6 +83,8 @@ def validate_y_range(cls, v: float, info) -> float:
class WorkspaceConfig(BaseModel):
"""Workspace configuration."""

model_config = ConfigDict(extra="forbid")

id: str = Field(..., description="Workspace identifier")
bounds: WorkspaceBounds = Field(..., description="Workspace boundaries")
center: list[float] = Field(..., description="Workspace center [x, y]")
Expand All @@ -94,6 +100,8 @@ def validate_center(cls, v: list[float]) -> list[float]:
class MotionConfig(BaseModel):
"""Robot motion parameters."""

model_config = ConfigDict(extra="forbid")

pick_z_offset: float = Field(0.001, ge=0.0, le=0.1, description="Z-offset for picking (m)")
place_z_offset: float = Field(0.001, ge=0.0, le=0.1, description="Z-offset for placing (m)")
safe_height: float = Field(0.15, ge=0.05, le=0.5, description="Safe height (m)")
Expand All @@ -106,6 +114,8 @@ class MotionConfig(BaseModel):
class RobotConfig(BaseModel):
"""Robot configuration settings."""

model_config = ConfigDict(extra="forbid")

type: str = Field("niryo", description="Robot type")
simulation: bool = Field(True, description="Use simulation mode")
verbose: bool = Field(False, description="Enable verbose output")
Expand All @@ -126,6 +136,8 @@ def validate_robot_type(cls, v: str) -> str:
class SpatialConfig(BaseModel):
"""Spatial query thresholds."""

model_config = ConfigDict(extra="forbid")

close_to_radius_m: float = Field(0.02, ge=0.001, le=0.5, description="'Close to' radius (m)")
left_right_threshold_m: float = Field(0.01, ge=0.001, le=0.1, description="Left/right threshold (m)")
above_below_threshold_m: float = Field(0.01, ge=0.001, le=0.1, description="Above/below threshold (m)")
Expand All @@ -134,6 +146,8 @@ class SpatialConfig(BaseModel):
class DetectionConfig(BaseModel):
"""Object detection settings."""

model_config = ConfigDict(extra="forbid")

model: str = Field("owlv2", description="Detection model")
device: str = Field("cuda", description="Computation device")
confidence_threshold: float = Field(0.15, ge=0.0, le=1.0, description="Confidence threshold")
Expand Down Expand Up @@ -162,6 +176,8 @@ def validate_device(cls, v: str) -> str:
class LLMProviderConfig(BaseModel):
"""LLM provider-specific settings."""

model_config = ConfigDict(extra="forbid")

enabled: bool = Field(True, description="Enable this provider")
default_model: str = Field(..., description="Default model name")
models: list[str] = Field(default_factory=list, description="Available models")
Expand All @@ -172,6 +188,8 @@ class LLMProviderConfig(BaseModel):
class LLMConfig(BaseModel):
"""LLM configuration settings."""

model_config = ConfigDict(extra="forbid")

default_provider: str = Field("auto", description="Default LLM provider")
temperature: float = Field(0.7, ge=0.0, le=2.0, description="Sampling temperature")
max_tokens: int = Field(4096, ge=1, le=128000, description="Maximum tokens")
Expand All @@ -192,6 +210,8 @@ def validate_provider(cls, v: str) -> str:
class TTSProviderConfig(BaseModel):
"""TTS provider settings."""

model_config = ConfigDict(extra="forbid")

voice_id: Optional[str] = None
voice: Optional[str] = None
stability: Optional[float] = Field(None, ge=0.0, le=1.0)
Expand All @@ -202,6 +222,8 @@ class TTSProviderConfig(BaseModel):
class TTSConfig(BaseModel):
"""Text-to-speech settings."""

model_config = ConfigDict(extra="forbid")

enabled: bool = Field(True, description="Enable TTS")
provider: str = Field("elevenlabs", description="TTS provider")
elevenlabs: TTSProviderConfig = Field(default_factory=TTSProviderConfig)
Expand All @@ -219,6 +241,8 @@ def validate_provider(cls, v: str) -> str:
class RedisStreamsConfig(BaseModel):
"""Redis stream names."""

model_config = ConfigDict(extra="forbid")

camera: str = Field("robot_camera", description="Camera stream name")
detected_objects: str = Field("detected_objects", description="Detected objects stream")
annotated_frames: str = Field("annotated_frames", description="Annotated frames stream")
Expand All @@ -227,6 +251,8 @@ class RedisStreamsConfig(BaseModel):
class RedisConfig(BaseModel):
"""Redis connection settings."""

model_config = ConfigDict(extra="forbid")

host: str = Field("localhost", description="Redis host")
port: int = Field(6379, ge=1, le=65535, description="Redis port")
db: int = Field(0, ge=0, le=15, description="Redis database number")
Expand All @@ -237,6 +263,8 @@ class RedisConfig(BaseModel):
class GUIConfig(BaseModel):
"""GUI settings."""

model_config = ConfigDict(extra="forbid")

host: str = Field("127.0.0.1", description="GUI host")
port: int = Field(7860, ge=1024, le=65535, description="GUI port")
share: bool = Field(False, description="Create public link")
Expand All @@ -248,13 +276,17 @@ class GUIConfig(BaseModel):
class LogRotationConfig(BaseModel):
"""Log rotation settings."""

model_config = ConfigDict(extra="forbid")

max_bytes: int = Field(10485760, ge=1024, description="Max log file size (bytes)")
backup_count: int = Field(5, ge=1, le=100, description="Number of backup files")


class LoggingConfig(BaseModel):
"""Logging configuration."""

model_config = ConfigDict(extra="forbid")

format: str = Field(..., description="Log message format")
date_format: str = Field("%Y-%m-%d %H:%M:%S", description="Date format")
levels: Dict[str, str] = Field(..., description="Log levels by module")
Expand All @@ -264,6 +296,8 @@ class LoggingConfig(BaseModel):
class EnvironmentOverrides(BaseModel):
"""Environment-specific configuration overrides."""

model_config = ConfigDict(extra="allow")

server: Optional[Dict[str, Any]] = None
robot: Optional[Dict[str, Any]] = None
llm: Optional[Dict[str, Any]] = None
Expand All @@ -272,6 +306,8 @@ class EnvironmentOverrides(BaseModel):
class RobotMCPConfig(BaseModel):
"""Root configuration model."""

model_config = ConfigDict(extra="forbid")

server: ServerConfig
robot: RobotConfig
detection: DetectionConfig
Expand Down
1 change: 1 addition & 0 deletions docs/en/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,7 @@ Add custom validators in `config_manager.py`:

```python
class ServerConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
port: int = Field(8000, ge=1024, le=65535)

@field_validator("port")
Expand Down
4 changes: 4 additions & 0 deletions robot_gui/mcp_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,10 @@ def __init__(

def _init_speech2text(self):
"""Initialize speech-to-text system."""
if Speech2Text is None:
# Already logged at module level, but good to be explicit here
return

try:
device = "cuda" if torch.cuda.is_available() else "cpu"
torch_dtype = torch.float16 if device == "cuda" else torch.float32
Expand Down
47 changes: 46 additions & 1 deletion server/fastmcp_robot_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,14 @@ def wrapper(*args, **kwargs):
mcp = FastMCP("robot-environment")
env = None
robot = None
current_robot_id = None


def initialize_environment(el_api_key="", use_simulation=True, robot_id="niryo", verbose=False, start_camera_thread=False):
"""Initialize the robot environment with given parameters."""
global env, robot
global env, robot, current_robot_id

current_robot_id = robot_id
logger.info("=" * 60)
logger.info("ENVIRONMENT INITIALIZATION")
logger.info(f" Robot ID: {robot_id}")
Expand All @@ -184,6 +186,49 @@ def initialize_environment(el_api_key="", use_simulation=True, robot_id="niryo",
# ============================================================================


@mcp.tool
@log_tool_call
def get_system_status() -> str:
"""
Get the current status of the robot system.

Returns the robot ID, the current workspace ID, and the current
gripper pose (x, y, z, roll, pitch, yaw).

Examples:
get_system_status()
# Returns: {
# "robot_id": "niryo",
# "workspace_id": "niryo_ws",
# "pose": {"x": 0.25, "y": 0.0, "z": 0.2, "roll": 0.0, "pitch": 1.57, "yaw": 0.0}
# }

Returns:
str: JSON string containing robot_id, workspace_id, and pose.
"""
try:
import json

pose = env.get_robot_pose()
workspace_id = env.get_current_workspace_id() or env.get_workspace_home_id()

status = {
"robot_id": current_robot_id,
"workspace_id": workspace_id,
"pose": {
"x": round(pose.x, 3),
"y": round(pose.y, 3),
"z": round(pose.z, 3),
"roll": round(pose.roll, 3),
"pitch": round(pose.pitch, 3),
"yaw": round(pose.yaw, 3),
},
}
return json.dumps(status, indent=2)
except Exception as e:
return f"❌ Error getting system status: {str(e)}"


@mcp.tool
@log_tool_call
def get_largest_free_space_with_center() -> str:
Expand Down
Loading
Loading