/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.ComponentModel;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using ModelContextProtocol.Server;
using Opc.Ua.Mcp.Serialization;
using Opc.Ua.Robotics.Client.Intent;
namespace Opc.Ua.Mcp.Tools
{
///
/// MCP tools for reading Robot Intent live state and outstanding work.
///
[McpServerToolType]
public sealed class RoboticsMonitoringTools
{
///
/// Reads one controller's current runtime state.
///
[McpServerTool(Name = "robotics_read_state")]
[Description("Reads live Robot Intent controller state now: operational mode, Ready, control owner, active " +
"intent/mission, safety state, and queue listings when published. This monitoring tool does not infer " +
"state from capabilities and does not request command authority.")]
public static async Task ReadStateAsync(
RoboticsIntentManager manager,
[Description(RoboticsControlTools.ControllerDescription)] string controller,
[Description("Session name to use; defaults to the only active session.")] string? sessionName = null,
CancellationToken ct = default)
{
RobotIntentControllerClient resolved = await manager.ResolveControllerAsync(
controller, sessionName, ct).ConfigureAwait(false);
return await resolved.ReadStateAsync(ct).ConfigureAwait(false);
}
///
/// Lists outstanding intent operations with filtering and paging.
///
[McpServerTool(Name = "robotics_list_operations")]
[Description("Lists Robot Intent operation snapshots published by the controller: active, queued, " +
"completed-retained, or failed intent-operation snapshots with their IntentId and operation NodeId. Use " +
"this after submitting or retrying one intent, or before robotics_wait_operation. Use " +
"robotics_list_missions instead when you need mission containers and MissionIds rather than per-intent " +
"operations. It reports server state only, never invents work, and never requests command authority. " +
"Returns a paged OperationListResult with summaries or full snapshots.")]
public static async Task ListOperationsAsync(
RoboticsIntentManager manager,
[Description(RoboticsControlTools.ControllerDescription)] string controller,
[Description("Optional query: filter by intentId, missionId, executionState, work selector; choose " +
"summary or full detail; page with pageSize (default 20, max 100) and cursor.")]
OperationListQuery? query = null,
[Description("Session name to use; defaults to the only active session.")] string? sessionName = null,
CancellationToken ct = default)
{
RobotIntentControllerClient resolved = await manager.ResolveControllerAsync(
controller, sessionName, ct).ConfigureAwait(false);
ArrayOf all = await resolved.ListOperationsAsync(ct).ConfigureAwait(false);
return RoboticsListPaging.PageOperations(all, query);
}
///
/// Lists outstanding missions with filtering and paging.
///
[McpServerTool(Name = "robotics_list_missions")]
[Description("Lists outstanding Robot Intent mission containers from the controller, including MissionId, " +
"update state, and mission-level progress when the server exposes it. Use this after submitting, " +
"updating, or cancelling a mission. Use robotics_list_operations instead to inspect the individual " +
"intent operations spawned by a mission or single-intent submission. It reports server state only, " +
"never invents mission state, and never requests command authority. Returns a paged MissionListResult " +
"with summaries or full snapshots.")]
public static async Task ListMissionsAsync(
RoboticsIntentManager manager,
[Description(RoboticsControlTools.ControllerDescription)] string controller,
[Description("Optional query: filter by missionId, executionState, work selector; choose summary or " +
"full detail; page with pageSize (default 20, max 100) and cursor.")]
MissionListQuery? query = null,
[Description("Session name to use; defaults to the only active session.")] string? sessionName = null,
CancellationToken ct = default)
{
RobotIntentControllerClient resolved = await manager.ResolveControllerAsync(
controller, sessionName, ct).ConfigureAwait(false);
ArrayOf all = await resolved.ListMissionsAsync(ct).ConfigureAwait(false);
return RoboticsListPaging.PageMissions(all, query);
}
///
/// Waits for an existing operation with a bounded timeout.
///
[McpServerTool(Name = "robotics_wait_operation")]
[Description("Waits for an intent operation to complete up to timeoutMs. Timeout is not an error: the " +
"result has completed=false and includes the current operation snapshot refreshed from the server. This " +
"tool does not retry refusals or resubmit work.")]
public static async Task WaitOperationAsync(
RoboticsIntentManager manager,
[Description(RoboticsControlTools.ControllerDescription)] string controller,
[Description("IntentId associated with the operation.")] string intentId,
[Description("IntentOperation NodeId to observe.")] string operationNodeId,
[Description("Maximum wait in milliseconds; <=0 performs a poll/refresh.")] int timeoutMs = 2000,
[Description("Session name to use; defaults to the only active session.")] string? sessionName = null,
CancellationToken ct = default)
{
RobotIntentControllerClient resolved = await manager.ResolveControllerAsync(
controller, sessionName, ct).ConfigureAwait(false);
return await WaitOperationCoreAsync(resolved, intentId, operationNodeId, timeoutMs, ct)
.ConfigureAwait(false);
}
internal static async Task WaitOperationCoreAsync(
RobotIntentControllerClient controller,
string intentId,
string operationNodeId,
int timeoutMs,
CancellationToken ct)
{
NodeId operation = OpcUaJsonHelper.ParseNodeId(operationNodeId);
IntentOperationHandle handle = await controller.TrackOperationAsync(
intentId,
operation,
ct).ConfigureAwait(false);
try
{
var timeout = TimeSpan.FromMilliseconds(timeoutMs <= 0 ? 0 : timeoutMs);
return await handle.WaitForCompletionAsync(timeout, ct).ConfigureAwait(false);
}
finally
{
await handle.DisposeAsync().ConfigureAwait(false);
}
}
///
/// Waits for an existing mission with a bounded timeout.
///
[McpServerTool(Name = "robotics_wait_mission")]
[Description("Waits for a Robot Intent mission to reach a terminal state, up to timeoutMs. The wait is " +
"always bounded and observes the mission node the server published; it never polls " +
"robotics_list_missions, never retries, and never resubmits or updates the mission. Timeout is not an " +
"error: the result has completed=false with the mission snapshot refreshed from the server. Command " +
"authority is never acquired as a side effect. Returns MissionWaitResult.")]
public static async Task WaitMissionAsync(
RoboticsIntentManager manager,
[Description(RoboticsControlTools.ControllerDescription)] string controller,
[Description("MissionId associated with the mission operation.")] string missionId,
[Description("Mission operation NodeId to observe, as returned by robotics_submit_mission or " +
"robotics_list_missions.")]
string missionNodeId,
[Description("Maximum wait in milliseconds; <=0 performs a poll/refresh. Bounded by " +
"MaxWaitMilliseconds.")]
int timeoutMs = 2000,
[Description("Session name to use; defaults to the only active session.")] string? sessionName = null,
CancellationToken ct = default)
{
RobotIntentControllerClient resolved = await manager.ResolveControllerAsync(
controller, sessionName, ct).ConfigureAwait(false);
return await WaitMissionCoreAsync(resolved, missionId, missionNodeId, timeoutMs, ct)
.ConfigureAwait(false);
}
internal static async Task WaitMissionCoreAsync(
RobotIntentControllerClient controller,
string missionId,
string missionNodeId,
int timeoutMs,
CancellationToken ct)
{
ArgumentNullException.ThrowIfNull(controller);
ArgumentException.ThrowIfNullOrWhiteSpace(missionId);
if (timeoutMs > MaxWaitMilliseconds)
{
throw new ArgumentException(
string.Create(CultureInfo.InvariantCulture,
$"'timeoutMs' must not exceed {MaxWaitMilliseconds} but was {timeoutMs}."),
nameof(timeoutMs));
}
NodeId missionNode = OpcUaJsonHelper.ParseNodeId(missionNodeId);
MissionHandle handle = await controller.TrackMissionAsync(
missionId,
missionNode,
ct).ConfigureAwait(false);
try
{
var timeout = TimeSpan.FromMilliseconds(timeoutMs <= 0 ? 0 : timeoutMs);
return await handle.WaitForCompletionAsync(timeout, ct).ConfigureAwait(false);
}
finally
{
await handle.DisposeAsync().ConfigureAwait(false);
}
}
private const int MaxWaitMilliseconds = 600000;
}
}