db1d565b64
Integration Tests / melodic (push) Has been cancelled
Integration Tests / noetic (push) Has been cancelled
Integration Tests / humble (push) Has been cancelled
Integration Tests / jazzy (push) Has been cancelled
Ruff Lint & Format / ruff (push) Has been cancelled
Sync main to develop / Check if sync is needed (push) Has been cancelled
Sync main to develop / Sync main to develop (push) Has been cancelled
328 lines
12 KiB
Python
328 lines
12 KiB
Python
"""Service tools for ROS MCP."""
|
|
|
|
from fastmcp import FastMCP
|
|
from mcp.types import ToolAnnotations
|
|
|
|
from ros_mcp.utils.response import _check_response, _safe_get_values
|
|
from ros_mcp.utils.rosapi_types import rosapi_service, rosapi_type
|
|
from ros_mcp.utils.websocket import WebSocketManager
|
|
|
|
|
|
def register_service_tools(
|
|
mcp: FastMCP,
|
|
ws_manager: WebSocketManager,
|
|
) -> None:
|
|
"""Register all service-related tools."""
|
|
|
|
@mcp.tool(
|
|
description=("Get list of all available ROS services.\nExample:\nget_services()"),
|
|
annotations=ToolAnnotations(
|
|
title="Get Services",
|
|
readOnlyHint=True,
|
|
),
|
|
)
|
|
def get_services() -> dict:
|
|
"""
|
|
Get list of all available ROS services.
|
|
|
|
Returns:
|
|
dict: Contains list of all active services,
|
|
or a message string if no services are found.
|
|
"""
|
|
message = {
|
|
"op": "call_service",
|
|
"service": rosapi_service("services"),
|
|
"type": rosapi_type("Services"),
|
|
"args": {},
|
|
"id": "get_services_request_1",
|
|
}
|
|
|
|
with ws_manager:
|
|
response = ws_manager.request(message)
|
|
|
|
error = _check_response(response)
|
|
if error:
|
|
return error
|
|
|
|
values = _safe_get_values(response)
|
|
if values is not None:
|
|
services = values.get("services", [])
|
|
return {"services": services, "service_count": len(services)}
|
|
return {"warning": "No services found"}
|
|
|
|
@mcp.tool(
|
|
description=(
|
|
"Get the service type for a specific service.\nExample:\nget_service_type('/rosapi/topics')"
|
|
),
|
|
annotations=ToolAnnotations(
|
|
title="Get Service Type",
|
|
readOnlyHint=True,
|
|
),
|
|
)
|
|
def get_service_type(service: str) -> dict:
|
|
"""
|
|
Get the service type for a specific service.
|
|
|
|
Args:
|
|
service (str): The service name (e.g., '/rosapi/topics')
|
|
|
|
Returns:
|
|
dict: Contains the service type,
|
|
or an error message if service doesn't exist.
|
|
"""
|
|
if not service or not service.strip():
|
|
return {"error": "Service name cannot be empty"}
|
|
|
|
message = {
|
|
"op": "call_service",
|
|
"service": rosapi_service("service_type"),
|
|
"type": rosapi_type("ServiceType"),
|
|
"args": {"service": service},
|
|
"id": f"get_service_type_request_{service.replace('/', '_')}",
|
|
}
|
|
|
|
with ws_manager:
|
|
response = ws_manager.request(message)
|
|
|
|
error = _check_response(response)
|
|
if error:
|
|
return error
|
|
|
|
values = _safe_get_values(response)
|
|
if values is not None:
|
|
service_type = values.get("type", "")
|
|
if service_type:
|
|
return {"service": service, "type": service_type}
|
|
return {"error": f"Service {service} does not exist or has no type"}
|
|
return {"error": f"Failed to get type for service {service}"}
|
|
|
|
@mcp.tool(
|
|
description=(
|
|
"Get complete service details including request/response structures and provider nodes.\n"
|
|
"Example:\n"
|
|
"get_service_details('/rosapi/topics')"
|
|
),
|
|
annotations=ToolAnnotations(
|
|
title="Get Service Details",
|
|
readOnlyHint=True,
|
|
),
|
|
)
|
|
def get_service_details(service: str) -> dict:
|
|
"""
|
|
Get complete service details including request/response structures and provider nodes.
|
|
|
|
Args:
|
|
service (str): The service name (e.g., '/rosapi/topics')
|
|
|
|
Returns:
|
|
dict: Contains complete service definition with request and response structures,
|
|
provider nodes, and provider count.
|
|
"""
|
|
# Validate input
|
|
if not service or not service.strip():
|
|
return {"error": "Service name cannot be empty"}
|
|
|
|
result = {
|
|
"service": service,
|
|
"type": "",
|
|
"request": {},
|
|
"response": {},
|
|
"providers": [],
|
|
"provider_count": 0,
|
|
}
|
|
|
|
with ws_manager:
|
|
# First get the service type
|
|
type_message = {
|
|
"op": "call_service",
|
|
"service": rosapi_service("service_type"),
|
|
"type": rosapi_type("ServiceType"),
|
|
"args": {"service": service},
|
|
"id": f"get_service_type_{service.replace('/', '_')}",
|
|
}
|
|
|
|
type_response = ws_manager.request(type_message)
|
|
error = _check_response(type_response)
|
|
if error:
|
|
return error
|
|
type_values = _safe_get_values(type_response)
|
|
if type_values is not None:
|
|
service_type = type_values.get("type", "")
|
|
if service_type:
|
|
result["type"] = service_type
|
|
else:
|
|
return {"error": f"Service {service} does not exist or has no type"}
|
|
else:
|
|
return {"error": f"Failed to get type for service {service}"}
|
|
|
|
# Get request details
|
|
request_message = {
|
|
"op": "call_service",
|
|
"service": rosapi_service("service_request_details"),
|
|
"type": rosapi_type("ServiceRequestDetails"),
|
|
"args": {"type": result["type"]},
|
|
"id": f"get_service_details_request_{result['type'].replace('/', '_')}",
|
|
}
|
|
|
|
request_response = ws_manager.request(request_message)
|
|
request_values = _safe_get_values(request_response)
|
|
if request_values is not None:
|
|
typedefs = request_values.get("typedefs", [])
|
|
if typedefs:
|
|
for typedef in typedefs:
|
|
field_names = typedef.get("fieldnames", [])
|
|
field_types = typedef.get("fieldtypes", [])
|
|
fields = {}
|
|
for name, ftype in zip(field_names, field_types):
|
|
fields[name] = ftype
|
|
result["request"] = {"fields": fields, "field_count": len(fields)}
|
|
|
|
# Get response details
|
|
response_message = {
|
|
"op": "call_service",
|
|
"service": rosapi_service("service_response_details"),
|
|
"type": rosapi_type("ServiceResponseDetails"),
|
|
"args": {"type": result["type"]},
|
|
"id": f"get_service_details_response_{result['type'].replace('/', '_')}",
|
|
}
|
|
|
|
response_response = ws_manager.request(response_message)
|
|
response_values = _safe_get_values(response_response)
|
|
if response_values is not None:
|
|
typedefs = response_values.get("typedefs", [])
|
|
if typedefs:
|
|
for typedef in typedefs:
|
|
field_names = typedef.get("fieldnames", [])
|
|
field_types = typedef.get("fieldtypes", [])
|
|
fields = {}
|
|
for name, ftype in zip(field_names, field_types):
|
|
fields[name] = ftype
|
|
result["response"] = {"fields": fields, "field_count": len(fields)}
|
|
|
|
# Get service providers
|
|
provider_message = {
|
|
"op": "call_service",
|
|
"service": rosapi_service("service_node"),
|
|
"type": rosapi_type("ServiceNode"),
|
|
"args": {"service": service},
|
|
"id": f"get_service_providers_request_{service.replace('/', '_')}",
|
|
}
|
|
|
|
provider_response = ws_manager.request(provider_message)
|
|
providers = []
|
|
|
|
# Handle different response formats safely
|
|
provider_values = _safe_get_values(provider_response)
|
|
if provider_values is not None:
|
|
node = provider_values.get("node", "")
|
|
if node:
|
|
providers = [node]
|
|
elif (
|
|
provider_response
|
|
and isinstance(provider_response, dict)
|
|
and isinstance(provider_response.get("result"), dict)
|
|
):
|
|
node = provider_response["result"].get("node", "")
|
|
if node:
|
|
providers = [node]
|
|
|
|
result["providers"] = providers
|
|
result["provider_count"] = len(providers)
|
|
|
|
# Check if we got any data
|
|
if not result["request"] and not result["response"]:
|
|
return {"error": f"Service {service} not found or has no definition"}
|
|
|
|
# Add note about field name format
|
|
result["note"] = (
|
|
"Field names shown above are formatted for rosbridge (leading underscores removed). "
|
|
"Use these exact field names when calling call_service()."
|
|
)
|
|
|
|
return result
|
|
|
|
@mcp.tool(
|
|
description=(
|
|
"Call a ROS service with specified request data.\n"
|
|
"Example:\n"
|
|
"call_service('/rosapi/topics', 'rosapi/Topics', {})\n"
|
|
"call_service('/slow_service', 'my_package/SlowService', {}, timeout=10.0) # Specify timeout only for slow services\n"
|
|
"\n"
|
|
"IMPORTANT: Field names in the request dict should match the field names shown by get_service_details(), "
|
|
"which are already formatted for rosbridge (without leading underscores). "
|
|
"For example, use {'topic': '/image'} not {'_topic': '/image'}."
|
|
),
|
|
annotations=ToolAnnotations(
|
|
title="Call Service",
|
|
destructiveHint=True,
|
|
),
|
|
)
|
|
def call_service(
|
|
service_name: str,
|
|
service_type: str,
|
|
request: dict,
|
|
timeout: float = None, # type: ignore[assignment] # See issue #140
|
|
) -> dict:
|
|
"""
|
|
Call a ROS service with specified request data.
|
|
|
|
Args:
|
|
service_name (str): The service name (e.g., '/rosapi/topics')
|
|
service_type (str): The service type (e.g., 'rosapi/Topics')
|
|
request (dict): Service request data as a dictionary
|
|
timeout (float): Timeout in seconds. If None, uses ws_manager.default_timeout.
|
|
|
|
Returns:
|
|
dict: Contains the service response or error information.
|
|
"""
|
|
# Use ws_manager.default_timeout if timeout is None
|
|
if timeout is None:
|
|
timeout = ws_manager.default_timeout
|
|
|
|
# rosbridge service call
|
|
message = {
|
|
"op": "call_service",
|
|
"service": service_name,
|
|
"type": service_type,
|
|
"args": request,
|
|
"id": f"call_service_request_{service_name.replace('/', '_')}",
|
|
}
|
|
|
|
# Call the service through rosbridge
|
|
with ws_manager:
|
|
response = ws_manager.request(message, timeout=timeout)
|
|
|
|
# Common error checks (no response, non-dict, ws_manager errors, result=false)
|
|
error = _check_response(response)
|
|
if error:
|
|
return {
|
|
"service": service_name,
|
|
"service_type": service_type,
|
|
"success": False,
|
|
**error,
|
|
}
|
|
|
|
# Return service response
|
|
if response.get("op") == "service_response":
|
|
return {
|
|
"service": service_name,
|
|
"service_type": service_type,
|
|
"success": response.get("result", True),
|
|
"result": response.get("values", {}),
|
|
}
|
|
elif response.get("op") == "status" and response.get("level") == "error":
|
|
return {
|
|
"service": service_name,
|
|
"service_type": service_type,
|
|
"success": False,
|
|
"error": response.get("msg", "Unknown error"),
|
|
}
|
|
else:
|
|
return {
|
|
"service": service_name,
|
|
"service_type": service_type,
|
|
"success": False,
|
|
"error": "Unexpected response format",
|
|
"raw_response": response,
|
|
}
|