Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:36:23 +08:00

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,
}