diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpAsyncClientWrapper.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpAsyncClientWrapper.java
index cf0327569..fb9ae7bff 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpAsyncClientWrapper.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpAsyncClientWrapper.java
@@ -15,6 +15,10 @@
*/
package io.agentscope.core.tool.mcp;
+import io.agentscope.core.tool.mcp.task.DefaultTaskManager;
+import io.agentscope.core.tool.mcp.task.ListTasksResult;
+import io.agentscope.core.tool.mcp.task.Task;
+import io.agentscope.core.tool.mcp.task.TaskManager;
import io.modelcontextprotocol.client.McpAsyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.List;
@@ -28,37 +32,66 @@
* This implementation delegates to {@link McpAsyncClient} and provides
* reactive operations that return Mono types.
*
- *
Example usage:
+ *
+ * Task Support Status:
+ *
+ * Task management infrastructure is available via
+ * {@link #getTaskManager()}
+ * ⚠️ Current Limitation: Task operations will throw
+ * {@link UnsupportedOperationException} because the underlying MCP SDK
+ * (version 0.17.0) does not yet provide native task support
+ * The task infrastructure is ready and will be automatically enabled
+ * when a future MCP SDK version adds task support
+ * For custom task implementations, you can extend this class and override
+ * {@link #createTaskManager()} to provide your own
+ * {@link io.agentscope.core.tool.mcp.task.DefaultTaskManager.TaskOperations}
+ *
+ *
+ *
+ * Example usage:
+ *
*
{@code
* McpAsyncClient client = ... // created via McpClient.async()
* McpAsyncClientWrapper wrapper = new McpAsyncClientWrapper("my-mcp", client);
* wrapper.initialize()
* .then(wrapper.callTool("tool_name", Map.of("arg1", "value1")))
* .subscribe(result -> System.out.println(result));
+ *
+ * // Task manager is available but operations will throw UnsupportedOperationException
+ * TaskManager taskManager = wrapper.getTaskManager();
+ * // taskManager.getTask("task-id").subscribe(...); // Will throw UnsupportedOperationException
* }
+ *
+ * @see McpClientWrapper#getTaskManager()
+ * @see io.agentscope.core.tool.mcp.task.TaskManager
*/
public class McpAsyncClientWrapper extends McpClientWrapper {
private static final Logger logger = LoggerFactory.getLogger(McpAsyncClientWrapper.class);
private final McpAsyncClient client;
+ private final TaskManager taskManager;
/**
* Constructs a new asynchronous MCP client wrapper.
*
- * @param name unique identifier for this client
+ * @param name unique identifier for this client
* @param client the underlying async MCP client
*/
public McpAsyncClientWrapper(String name, McpAsyncClient client) {
super(name);
this.client = client;
+ this.taskManager = createTaskManager();
}
/**
* Initializes the async MCP client connection and caches available tools.
*
- * This method connects to the MCP server, discovers available tools, and caches them for
- * later use. If already initialized, this method returns immediately without re-initializing.
+ *
+ * This method connects to the MCP server, discovers available tools, and caches
+ * them for
+ * later use. If already initialized, this method returns immediately without
+ * re-initializing.
*
* @return a Mono that completes when initialization is finished
*/
@@ -95,7 +128,9 @@ public Mono initialize() {
/**
* Lists all tools available from the MCP server.
*
- * This method queries the MCP server for its current list of tools. The client must be
+ *
+ * This method queries the MCP server for its current list of tools. The client
+ * must be
* initialized before calling this method.
*
* @return a Mono emitting the list of available tools
@@ -114,10 +149,12 @@ public Mono> listTools() {
/**
* Invokes a tool on the MCP server asynchronously.
*
- * This method sends a tool call request to the MCP server and returns the result
+ *
+ * This method sends a tool call request to the MCP server and returns the
+ * result
* asynchronously. The client must be initialized before calling this method.
*
- * @param toolName the name of the tool to call
+ * @param toolName the name of the tool to call
* @param arguments the arguments to pass to the tool
* @return a Mono emitting the tool call result (may contain error information)
* @throws IllegalStateException if the client is not initialized
@@ -153,11 +190,73 @@ public Mono callTool(String toolName, Map
+ * This method creates a DefaultTaskManager with task operations that delegate
+ * to the underlying MCP client. Note that task support depends on the MCP SDK
+ * version and server capabilities.
+ *
+ * @return a new TaskManager instance
+ */
+ private TaskManager createTaskManager() {
+ DefaultTaskManager.TaskOperations operations =
+ new DefaultTaskManager.TaskOperations() {
+ @Override
+ public Mono getTask(String taskId) {
+ // TODO: Implement when MCP SDK supports tasks/get
+ return Mono.error(
+ new UnsupportedOperationException(
+ "Task operations not yet supported by MCP SDK"));
+ }
+
+ @Override
+ public Mono getTaskResult(String taskId) {
+ // TODO: Implement when MCP SDK supports tasks/result
+ return Mono.error(
+ new UnsupportedOperationException(
+ "Task operations not yet supported by MCP SDK"));
+ }
+
+ @Override
+ public Mono listTasks(String cursor) {
+ // TODO: Implement when MCP SDK supports tasks/list
+ return Mono.error(
+ new UnsupportedOperationException(
+ "Task operations not yet supported by MCP SDK"));
+ }
+
+ @Override
+ public Mono cancelTask(String taskId) {
+ // TODO: Implement when MCP SDK supports tasks/cancel
+ return Mono.error(
+ new UnsupportedOperationException(
+ "Task operations not yet supported by MCP SDK"));
+ }
+ };
+
+ return new DefaultTaskManager(name, operations);
+ }
+
/**
* Closes the MCP client connection and releases all resources.
*
- * This method attempts to close the client gracefully, falling back to forceful closure if
- * graceful closure fails. This method is idempotent and can be called multiple times safely.
+ *
+ * This method attempts to close the client gracefully, falling back to forceful
+ * closure if
+ * graceful closure fails. This method is idempotent and can be called multiple
+ * times safely.
*/
@Override
public void close() {
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientWrapper.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientWrapper.java
index 449deda74..7292930e5 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientWrapper.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpClientWrapper.java
@@ -15,6 +15,7 @@
*/
package io.agentscope.core.tool.mcp;
+import io.agentscope.core.tool.mcp.task.TaskManager;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.List;
import java.util.Map;
@@ -24,14 +25,17 @@
/**
* Abstract wrapper for MCP (Model Context Protocol) clients.
* This class manages the lifecycle of MCP client connections and provides
- * a unified interface for both asynchronous and synchronous client implementations.
+ * a unified interface for both asynchronous and synchronous client
+ * implementations.
*
- *
The wrapper handles:
+ *
+ * The wrapper handles:
*
- * Client initialization and connection management
- * Tool discovery and caching
- * Tool invocation through the MCP protocol
- * Resource cleanup on close
+ * Client initialization and connection management
+ * Tool discovery and caching
+ * Tool invocation through the MCP protocol
+ * Task management for long-running operations
+ * Resource cleanup on close
*
*
* @see McpAsyncClientWrapper
@@ -94,7 +98,7 @@ public boolean isInitialized() {
/**
* Invokes a tool on the MCP server.
*
- * @param toolName the name of the tool to call
+ * @param toolName the name of the tool to call
* @param arguments the arguments to pass to the tool
* @return a Mono emitting the tool call result
*/
@@ -111,6 +115,18 @@ public McpSchema.Tool getCachedTool(String toolName) {
return cachedTools.get(toolName);
}
+ /**
+ * Gets the task manager for this MCP client.
+ *
+ *
+ * The task manager provides methods for managing long-running operations,
+ * including retrieving task status, getting results, listing tasks, and
+ * cancelling tasks.
+ *
+ * @return the task manager instance
+ */
+ public abstract TaskManager getTaskManager();
+
/**
* Closes this MCP client and releases all resources.
* This method is idempotent and can be called multiple times safely.
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpSyncClientWrapper.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpSyncClientWrapper.java
index bbd0e2186..7574fa968 100644
--- a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpSyncClientWrapper.java
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/McpSyncClientWrapper.java
@@ -15,6 +15,10 @@
*/
package io.agentscope.core.tool.mcp;
+import io.agentscope.core.tool.mcp.task.DefaultTaskManager;
+import io.agentscope.core.tool.mcp.task.ListTasksResult;
+import io.agentscope.core.tool.mcp.task.Task;
+import io.agentscope.core.tool.mcp.task.TaskManager;
import io.modelcontextprotocol.client.McpSyncClient;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.List;
@@ -25,41 +29,72 @@
import reactor.core.scheduler.Schedulers;
/**
- * Wrapper for synchronous MCP clients that converts blocking calls to reactive Mono types.
- * This implementation delegates to {@link McpSyncClient} and wraps blocking operations
+ * Wrapper for synchronous MCP clients that converts blocking calls to reactive
+ * Mono types.
+ * This implementation delegates to {@link McpSyncClient} and wraps blocking
+ * operations
* in Reactor's boundedElastic scheduler to avoid blocking the event loop.
*
- *
Example usage:
+ *
+ * Task Support Status:
+ *
+ * Task management infrastructure is available via
+ * {@link #getTaskManager()}
+ * ⚠️ Current Limitation: Task operations will throw
+ * {@link UnsupportedOperationException} because the underlying MCP SDK
+ * (version 0.17.0) does not yet provide native task support
+ * The task infrastructure is ready and will be automatically enabled
+ * when a future MCP SDK version adds task support
+ * For custom task implementations, you can extend this class and override
+ * {@link #createTaskManager()} to provide your own
+ * {@link io.agentscope.core.tool.mcp.task.DefaultTaskManager.TaskOperations}
+ *
+ *
+ *
+ * Example usage:
+ *
*
{@code
* McpSyncClient client = ... // created via McpClient.sync()
* McpSyncClientWrapper wrapper = new McpSyncClientWrapper("my-mcp", client);
* wrapper.initialize()
* .then(wrapper.callTool("tool_name", Map.of("arg1", "value1")))
* .subscribe(result -> System.out.println(result));
+ *
+ * // Task manager is available but operations will throw UnsupportedOperationException
+ * TaskManager taskManager = wrapper.getTaskManager();
+ * // taskManager.getTask("task-id").subscribe(...); // Will throw UnsupportedOperationException
* }
+ *
+ * @see McpClientWrapper#getTaskManager()
+ * @see io.agentscope.core.tool.mcp.task.TaskManager
*/
public class McpSyncClientWrapper extends McpClientWrapper {
private static final Logger logger = LoggerFactory.getLogger(McpSyncClientWrapper.class);
private final McpSyncClient client;
+ private final TaskManager taskManager;
/**
* Constructs a new synchronous MCP client wrapper.
*
- * @param name unique identifier for this client
+ * @param name unique identifier for this client
* @param client the underlying sync MCP client
*/
public McpSyncClientWrapper(String name, McpSyncClient client) {
super(name);
this.client = client;
+ this.taskManager = createTaskManager();
}
/**
* Initializes the sync MCP client connection and caches available tools.
*
- * This method wraps the blocking synchronous client operations in a reactive Mono that runs
- * on the boundedElastic scheduler to avoid blocking the event loop. If already initialized,
+ *
+ * This method wraps the blocking synchronous client operations in a reactive
+ * Mono that runs
+ * on the boundedElastic scheduler to avoid blocking the event loop. If already
+ * initialized,
* this method returns immediately without re-initializing.
*
* @return a Mono that completes when initialization is finished
@@ -101,7 +136,9 @@ public Mono initialize() {
/**
* Lists all tools available from the MCP server.
*
- * This method wraps the blocking synchronous listTools call in a reactive Mono. The client
+ *
+ * This method wraps the blocking synchronous listTools call in a reactive Mono.
+ * The client
* must be initialized before calling this method.
*
* @return a Mono emitting the list of available tools
@@ -119,12 +156,16 @@ public Mono> listTools() {
}
/**
- * Invokes a tool on the MCP server, wrapping the blocking call in a reactive Mono.
+ * Invokes a tool on the MCP server, wrapping the blocking call in a reactive
+ * Mono.
*
- * This method wraps the blocking synchronous callTool operation in a Mono that runs on the
- * boundedElastic scheduler. The client must be initialized before calling this method.
+ *
+ * This method wraps the blocking synchronous callTool operation in a Mono that
+ * runs on the
+ * boundedElastic scheduler. The client must be initialized before calling this
+ * method.
*
- * @param toolName the name of the tool to call
+ * @param toolName the name of the tool to call
* @param arguments the arguments to pass to the tool
* @return a Mono emitting the tool call result (may contain error information)
* @throws IllegalStateException if the client is not initialized
@@ -164,11 +205,73 @@ public Mono callTool(String toolName, Map
+ * This method creates a DefaultTaskManager with task operations that delegate
+ * to the underlying MCP client. Note that task support depends on the MCP SDK
+ * version and server capabilities.
+ *
+ * @return a new TaskManager instance
+ */
+ private TaskManager createTaskManager() {
+ DefaultTaskManager.TaskOperations operations =
+ new DefaultTaskManager.TaskOperations() {
+ @Override
+ public Mono getTask(String taskId) {
+ // TODO: Implement when MCP SDK supports tasks/get
+ return Mono.error(
+ new UnsupportedOperationException(
+ "Task operations not yet supported by MCP SDK"));
+ }
+
+ @Override
+ public Mono getTaskResult(String taskId) {
+ // TODO: Implement when MCP SDK supports tasks/result
+ return Mono.error(
+ new UnsupportedOperationException(
+ "Task operations not yet supported by MCP SDK"));
+ }
+
+ @Override
+ public Mono listTasks(String cursor) {
+ // TODO: Implement when MCP SDK supports tasks/list
+ return Mono.error(
+ new UnsupportedOperationException(
+ "Task operations not yet supported by MCP SDK"));
+ }
+
+ @Override
+ public Mono cancelTask(String taskId) {
+ // TODO: Implement when MCP SDK supports tasks/cancel
+ return Mono.error(
+ new UnsupportedOperationException(
+ "Task operations not yet supported by MCP SDK"));
+ }
+ };
+
+ return new DefaultTaskManager(name, operations);
+ }
+
/**
* Closes the MCP client connection and releases all resources.
*
- * This method attempts to close the client gracefully, falling back to forceful closure if
- * graceful closure fails. This method is idempotent and can be called multiple times safely.
+ *
+ * This method attempts to close the client gracefully, falling back to forceful
+ * closure if
+ * graceful closure fails. This method is idempotent and can be called multiple
+ * times safely.
*/
@Override
public void close() {
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/DefaultTaskManager.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/DefaultTaskManager.java
new file mode 100644
index 000000000..e9c937eab
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/DefaultTaskManager.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import reactor.core.publisher.Mono;
+
+/**
+ * Default implementation of TaskManager for MCP clients.
+ *
+ *
+ * This implementation provides task management capabilities by delegating
+ * to the underlying MCP client's task-related methods. It maintains a cache
+ * of task status listeners and handles task status notifications.
+ *
+ *
+ * Note: This is a basic implementation that assumes the MCP SDK provides
+ * the necessary task-related methods. If the SDK doesn't support tasks
+ * natively,
+ * this implementation will need to be extended to handle task operations
+ * through
+ * custom protocol messages.
+ */
+public class DefaultTaskManager implements TaskManager {
+
+ private static final Logger logger = LoggerFactory.getLogger(DefaultTaskManager.class);
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private final String clientName;
+ private final TaskOperations taskOperations;
+ private final List listeners = new CopyOnWriteArrayList<>();
+ private final Map taskCache = new ConcurrentHashMap<>();
+
+ /**
+ * Constructs a new DefaultTaskManager.
+ *
+ * @param clientName the name of the MCP client
+ * @param taskOperations the task operations provider
+ */
+ public DefaultTaskManager(String clientName, TaskOperations taskOperations) {
+ this.clientName = clientName;
+ this.taskOperations = taskOperations;
+ }
+
+ @Override
+ public Mono getTask(String taskId) {
+ logger.debug("Getting task '{}' from client '{}'", taskId, clientName);
+
+ return taskOperations
+ .getTask(taskId)
+ .doOnNext(
+ task -> {
+ taskCache.put(taskId, task);
+ notifyListeners(task);
+ })
+ .doOnError(
+ e -> logger.error("Failed to get task '{}': {}", taskId, e.getMessage()));
+ }
+
+ @Override
+ public Mono getTaskResult(String taskId, Class resultType) {
+ logger.debug("Getting result for task '{}' from client '{}'", taskId, clientName);
+
+ return taskOperations
+ .getTaskResult(taskId)
+ .map(
+ result -> {
+ try {
+ // Convert the result to the expected type
+ if (resultType.isInstance(result)) {
+ return resultType.cast(result);
+ }
+ // Try JSON conversion if direct cast fails
+ JsonNode jsonNode = objectMapper.valueToTree(result);
+ return objectMapper.treeToValue(jsonNode, resultType);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Failed to convert task result to " + resultType.getName(),
+ e);
+ }
+ })
+ .doOnError(
+ e ->
+ logger.error(
+ "Failed to get result for task '{}': {}",
+ taskId,
+ e.getMessage()));
+ }
+
+ @Override
+ public Mono listTasks(String cursor) {
+ logger.debug("Listing tasks from client '{}' with cursor '{}'", clientName, cursor);
+
+ return taskOperations
+ .listTasks(cursor)
+ .doOnNext(
+ result -> {
+ // Cache all tasks
+ result.getTasks()
+ .forEach(task -> taskCache.put(task.getTaskId(), task));
+ })
+ .doOnError(e -> logger.error("Failed to list tasks: {}", e.getMessage()));
+ }
+
+ @Override
+ public Mono cancelTask(String taskId) {
+ logger.debug("Cancelling task '{}' on client '{}'", taskId, clientName);
+
+ return taskOperations
+ .cancelTask(taskId)
+ .doOnNext(
+ task -> {
+ taskCache.put(taskId, task);
+ notifyListeners(task);
+ })
+ .doOnError(
+ e ->
+ logger.error(
+ "Failed to cancel task '{}': {}", taskId, e.getMessage()));
+ }
+
+ @Override
+ public void registerTaskStatusListener(TaskStatusListener listener) {
+ if (listener != null && !listeners.contains(listener)) {
+ listeners.add(listener);
+ logger.debug("Registered task status listener for client '{}'", clientName);
+ }
+ }
+
+ @Override
+ public void unregisterTaskStatusListener(TaskStatusListener listener) {
+ if (listener != null && listeners.remove(listener)) {
+ logger.debug("Unregistered task status listener for client '{}'", clientName);
+ }
+ }
+
+ /**
+ * Handles incoming task status notifications.
+ *
+ *
+ * This method should be called by the MCP client when it receives
+ * a notifications/tasks/status message from the server.
+ *
+ * @param task the updated task information
+ */
+ public void handleTaskStatusNotification(Task task) {
+ logger.debug(
+ "Received task status notification for task '{}': {}",
+ task.getTaskId(),
+ task.getStatus());
+ taskCache.put(task.getTaskId(), task);
+ notifyListeners(task);
+ }
+
+ /**
+ * Notifies all registered listeners of a task status change.
+ *
+ * @param task the updated task
+ */
+ private void notifyListeners(Task task) {
+ for (TaskStatusListener listener : listeners) {
+ try {
+ listener.onTaskStatusChanged(task);
+ } catch (Exception e) {
+ logger.error("Error notifying task status listener", e);
+ }
+ }
+ }
+
+ /**
+ * Gets a cached task by ID.
+ *
+ * @param taskId the task ID
+ * @return the cached task, or null if not found
+ */
+ public Task getCachedTask(String taskId) {
+ return taskCache.get(taskId);
+ }
+
+ /**
+ * Gets all cached tasks.
+ *
+ * @return a list of all cached tasks
+ */
+ public List getAllCachedTasks() {
+ return new ArrayList<>(taskCache.values());
+ }
+
+ /**
+ * Clears the task cache.
+ */
+ public void clearCache() {
+ taskCache.clear();
+ logger.debug("Cleared task cache for client '{}'", clientName);
+ }
+
+ /**
+ * Interface for task operations that must be provided by the MCP client.
+ */
+ public interface TaskOperations {
+ /**
+ * Gets a task by ID.
+ *
+ * @param taskId the task ID
+ * @return a Mono emitting the task
+ */
+ Mono getTask(String taskId);
+
+ /**
+ * Gets the result of a task.
+ *
+ * @param taskId the task ID
+ * @return a Mono emitting the task result
+ */
+ Mono getTaskResult(String taskId);
+
+ /**
+ * Lists tasks with optional pagination.
+ *
+ * @param cursor optional cursor for pagination
+ * @return a Mono emitting the list result
+ */
+ Mono listTasks(String cursor);
+
+ /**
+ * Cancels a task.
+ *
+ * @param taskId the task ID
+ * @return a Mono emitting the updated task
+ */
+ Mono cancelTask(String taskId);
+ }
+
+ /**
+ * Creates a Task from a raw JSON response.
+ *
+ *
+ * This utility method helps convert MCP protocol responses into Task objects.
+ *
+ * @param taskId the task ID
+ * @param status the status string
+ * @param statusMessage optional status message
+ * @param createdAt creation timestamp string (ISO 8601)
+ * @param lastUpdatedAt last update timestamp string (ISO 8601)
+ * @param ttl time-to-live in milliseconds
+ * @param pollInterval poll interval in milliseconds
+ * @return a new Task instance
+ */
+ public static Task createTask(
+ String taskId,
+ String status,
+ String statusMessage,
+ String createdAt,
+ String lastUpdatedAt,
+ Long ttl,
+ Long pollInterval) {
+ return Task.builder()
+ .taskId(taskId)
+ .status(TaskStatus.fromValue(status))
+ .statusMessage(statusMessage)
+ .createdAt(Instant.parse(createdAt))
+ .lastUpdatedAt(Instant.parse(lastUpdatedAt))
+ .ttl(ttl)
+ .pollInterval(pollInterval)
+ .build();
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/ListTasksResult.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/ListTasksResult.java
new file mode 100644
index 000000000..34c0dc98e
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/ListTasksResult.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Result of listing tasks from an MCP server.
+ *
+ *
+ * This class encapsulates the response from a tasks/list request,
+ * which includes a list of tasks and an optional cursor for pagination.
+ *
+ * @see Task
+ * @see MCP
+ * Tasks Specification
+ */
+public class ListTasksResult {
+
+ private final List tasks;
+ private final String nextCursor;
+
+ /**
+ * Constructs a new ListTasksResult.
+ *
+ * @param tasks the list of tasks
+ * @param nextCursor optional cursor for retrieving the next page of results
+ */
+ public ListTasksResult(List tasks, String nextCursor) {
+ this.tasks =
+ Collections.unmodifiableList(Objects.requireNonNull(tasks, "tasks cannot be null"));
+ this.nextCursor = nextCursor;
+ }
+
+ /**
+ * Gets an unmodifiable view of the tasks list.
+ *
+ *
+ * The returned list cannot be modified. Any attempt to modify it will throw
+ * {@link UnsupportedOperationException}.
+ *
+ * @return an unmodifiable list of tasks
+ */
+ public List getTasks() {
+ return tasks;
+ }
+
+ /**
+ * Gets the cursor for the next page of results.
+ *
+ * @return the next cursor, or null if there are no more results
+ */
+ public String getNextCursor() {
+ return nextCursor;
+ }
+
+ /**
+ * Checks if there are more results available.
+ *
+ * @return true if a next cursor is present, false otherwise
+ */
+ public boolean hasMore() {
+ return nextCursor != null && !nextCursor.isEmpty();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ ListTasksResult that = (ListTasksResult) o;
+ return Objects.equals(tasks, that.tasks) && Objects.equals(nextCursor, that.nextCursor);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(tasks, nextCursor);
+ }
+
+ @Override
+ public String toString() {
+ return "ListTasksResult{" + "tasks=" + tasks + ", nextCursor='" + nextCursor + '\'' + '}';
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/Task.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/Task.java
new file mode 100644
index 000000000..7aa8a456d
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/Task.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import java.time.Instant;
+import java.util.Objects;
+
+/**
+ * Represents a task in the MCP (Model Context Protocol) system.
+ *
+ *
+ * A task encapsulates the state and metadata of a long-running operation
+ * that may not complete immediately. Tasks allow for asynchronous execution
+ * with status tracking and result retrieval.
+ *
+ *
+ * Key properties:
+ *
+ * taskId - Unique identifier for the task
+ * status - Current state of the task execution
+ * statusMessage - Optional human-readable message describing the current
+ * state
+ * createdAt - ISO 8601 timestamp when the task was created
+ * lastUpdatedAt - ISO 8601 timestamp when the task status was last
+ * updated
+ * ttl - Time in milliseconds from creation before task may be deleted
+ * pollInterval - Suggested time in milliseconds between status checks
+ *
+ *
+ * @see TaskStatus
+ * @see MCP
+ * Tasks Specification
+ */
+public class Task {
+
+ private final String taskId;
+ private final TaskStatus status;
+ private final String statusMessage;
+ private final Instant createdAt;
+ private final Instant lastUpdatedAt;
+ private final Long ttl;
+ private final Long pollInterval;
+
+ /**
+ * Constructs a new Task instance.
+ *
+ * @param taskId unique identifier for the task
+ * @param status current state of the task execution
+ * @param statusMessage optional human-readable message describing the current
+ * state
+ * @param createdAt timestamp when the task was created
+ * @param lastUpdatedAt timestamp when the task status was last updated
+ * @param ttl time in milliseconds from creation before task may be
+ * deleted
+ * @param pollInterval suggested time in milliseconds between status checks
+ */
+ public Task(
+ String taskId,
+ TaskStatus status,
+ String statusMessage,
+ Instant createdAt,
+ Instant lastUpdatedAt,
+ Long ttl,
+ Long pollInterval) {
+ this.taskId = Objects.requireNonNull(taskId, "taskId cannot be null");
+ this.status = Objects.requireNonNull(status, "status cannot be null");
+ this.statusMessage = statusMessage;
+ this.createdAt = Objects.requireNonNull(createdAt, "createdAt cannot be null");
+ this.lastUpdatedAt = Objects.requireNonNull(lastUpdatedAt, "lastUpdatedAt cannot be null");
+ this.ttl = ttl;
+ this.pollInterval = pollInterval;
+ }
+
+ /**
+ * Gets the unique identifier for this task.
+ *
+ * @return the task ID
+ */
+ public String getTaskId() {
+ return taskId;
+ }
+
+ /**
+ * Gets the current status of this task.
+ *
+ * @return the task status
+ */
+ public TaskStatus getStatus() {
+ return status;
+ }
+
+ /**
+ * Gets the optional human-readable status message.
+ *
+ * @return the status message, or null if not provided
+ */
+ public String getStatusMessage() {
+ return statusMessage;
+ }
+
+ /**
+ * Gets the timestamp when this task was created.
+ *
+ * @return the creation timestamp
+ */
+ public Instant getCreatedAt() {
+ return createdAt;
+ }
+
+ /**
+ * Gets the timestamp when this task was last updated.
+ *
+ * @return the last update timestamp
+ */
+ public Instant getLastUpdatedAt() {
+ return lastUpdatedAt;
+ }
+
+ /**
+ * Gets the time-to-live in milliseconds from creation.
+ *
+ * @return the TTL in milliseconds, or null if not specified
+ */
+ public Long getTtl() {
+ return ttl;
+ }
+
+ /**
+ * Gets the suggested polling interval in milliseconds.
+ *
+ * @return the poll interval in milliseconds, or null if not specified
+ */
+ public Long getPollInterval() {
+ return pollInterval;
+ }
+
+ /**
+ * Checks if this task is in a terminal state.
+ *
+ * @return true if the task has completed, failed, or been cancelled
+ */
+ public boolean isTerminal() {
+ return status.isTerminal();
+ }
+
+ /**
+ * Creates a builder for constructing Task instances.
+ *
+ * @return a new TaskBuilder
+ */
+ public static TaskBuilder builder() {
+ return new TaskBuilder();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Task task = (Task) o;
+ return Objects.equals(taskId, task.taskId)
+ && status == task.status
+ && Objects.equals(statusMessage, task.statusMessage)
+ && Objects.equals(createdAt, task.createdAt)
+ && Objects.equals(lastUpdatedAt, task.lastUpdatedAt)
+ && Objects.equals(ttl, task.ttl)
+ && Objects.equals(pollInterval, task.pollInterval);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ taskId, status, statusMessage, createdAt, lastUpdatedAt, ttl, pollInterval);
+ }
+
+ @Override
+ public String toString() {
+ return "Task{"
+ + "taskId='"
+ + taskId
+ + '\''
+ + ", status="
+ + status
+ + ", statusMessage='"
+ + statusMessage
+ + '\''
+ + ", createdAt="
+ + createdAt
+ + ", lastUpdatedAt="
+ + lastUpdatedAt
+ + ", ttl="
+ + ttl
+ + ", pollInterval="
+ + pollInterval
+ + '}';
+ }
+
+ /**
+ * Builder for creating Task instances.
+ */
+ public static class TaskBuilder {
+ private String taskId;
+ private TaskStatus status;
+ private String statusMessage;
+ private Instant createdAt;
+ private Instant lastUpdatedAt;
+ private Long ttl;
+ private Long pollInterval;
+
+ public TaskBuilder taskId(String taskId) {
+ this.taskId = taskId;
+ return this;
+ }
+
+ public TaskBuilder status(TaskStatus status) {
+ this.status = status;
+ return this;
+ }
+
+ public TaskBuilder statusMessage(String statusMessage) {
+ this.statusMessage = statusMessage;
+ return this;
+ }
+
+ public TaskBuilder createdAt(Instant createdAt) {
+ this.createdAt = createdAt;
+ return this;
+ }
+
+ public TaskBuilder lastUpdatedAt(Instant lastUpdatedAt) {
+ this.lastUpdatedAt = lastUpdatedAt;
+ return this;
+ }
+
+ public TaskBuilder ttl(Long ttl) {
+ this.ttl = ttl;
+ return this;
+ }
+
+ public TaskBuilder pollInterval(Long pollInterval) {
+ this.pollInterval = pollInterval;
+ return this;
+ }
+
+ public Task build() {
+ return new Task(
+ taskId, status, statusMessage, createdAt, lastUpdatedAt, ttl, pollInterval);
+ }
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskManager.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskManager.java
new file mode 100644
index 000000000..16a5ac1a2
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskManager.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import reactor.core.publisher.Mono;
+
+/**
+ * Interface for managing MCP tasks.
+ *
+ *
+ * This interface provides methods for interacting with tasks in the MCP
+ * protocol,
+ * including retrieving task status, getting results, listing tasks, and
+ * cancelling tasks.
+ *
+ *
+ * All methods return Mono types for reactive programming support, allowing for
+ * asynchronous and non-blocking operations.
+ *
+ * @see Task
+ * @see TaskStatus
+ * @see MCP
+ * Tasks Specification
+ */
+public interface TaskManager {
+
+ /**
+ * Gets the current status and metadata of a task.
+ *
+ *
+ * This method queries the server for the current state of a task.
+ * It can be called repeatedly to poll for task completion.
+ *
+ * @param taskId the unique identifier of the task
+ * @return a Mono emitting the task information
+ */
+ Mono getTask(String taskId);
+
+ /**
+ * Retrieves the result of a completed task.
+ *
+ *
+ * This method should be called when a task reaches a terminal state
+ * (completed, failed, or cancelled) or when the task status is input_required.
+ *
+ *
+ * The result type depends on the original request that created the task:
+ *
+ * For tool calls: returns CallToolResult
+ * For sampling requests: returns appropriate sampling result
+ *
+ *
+ * @param taskId the unique identifier of the task
+ * @param resultType the expected result type class
+ * @param the type of the result
+ * @return a Mono emitting the task result
+ */
+ Mono getTaskResult(String taskId, Class resultType);
+
+ /**
+ * Lists all tasks known to the server.
+ *
+ *
+ * This method supports pagination through the cursor parameter.
+ * If the result contains a nextCursor, it can be used to retrieve
+ * the next page of results.
+ *
+ * @param cursor optional cursor for pagination (null for first page)
+ * @return a Mono emitting the list of tasks with pagination info
+ */
+ Mono listTasks(String cursor);
+
+ /**
+ * Cancels a running task.
+ *
+ *
+ * This method requests cancellation of a task. The server will attempt
+ * to stop the task execution and transition it to the CANCELLED state.
+ *
+ *
+ * Note that cancellation may not be immediate, and some tasks may not
+ * be cancellable depending on their current state.
+ *
+ * @param taskId the unique identifier of the task to cancel
+ * @return a Mono emitting the updated task information after cancellation
+ */
+ Mono cancelTask(String taskId);
+
+ /**
+ * Registers a listener for task status notifications.
+ *
+ *
+ * When registered, the listener will be notified whenever a task's
+ * status changes. This allows for event-driven task monitoring instead
+ * of polling.
+ *
+ * @param listener the task status listener
+ */
+ void registerTaskStatusListener(TaskStatusListener listener);
+
+ /**
+ * Unregisters a previously registered task status listener.
+ *
+ * @param listener the task status listener to remove
+ */
+ void unregisterTaskStatusListener(TaskStatusListener listener);
+
+ /**
+ * Listener interface for task status change notifications.
+ */
+ @FunctionalInterface
+ interface TaskStatusListener {
+ /**
+ * Called when a task's status changes.
+ *
+ * @param task the updated task information
+ */
+ void onTaskStatusChanged(Task task);
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskParameters.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskParameters.java
new file mode 100644
index 000000000..27bfb24f4
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskParameters.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import java.util.Objects;
+
+/**
+ * Parameters for creating a task in the MCP protocol.
+ *
+ *
+ * These parameters can be included in requests (such as tool calls or sampling
+ * requests)
+ * to indicate that the operation should be executed as a task rather than
+ * returning
+ * results immediately.
+ *
+ *
+ * When task parameters are included in a request, the server will:
+ *
+ * Accept the request and immediately return a CreateTaskResult
+ * Execute the operation asynchronously
+ * Make results available later through tasks/result
+ *
+ *
+ * @see MCP
+ * Tasks Specification
+ */
+public class TaskParameters {
+
+ private final Long ttl;
+
+ /**
+ * Constructs task parameters with the specified TTL.
+ *
+ * @param ttl requested duration in milliseconds to retain task from creation
+ */
+ public TaskParameters(Long ttl) {
+ this.ttl = ttl;
+ }
+
+ /**
+ * Gets the requested time-to-live for the task.
+ *
+ * @return the TTL in milliseconds, or null if not specified
+ */
+ public Long getTtl() {
+ return ttl;
+ }
+
+ /**
+ * Creates task parameters with the specified TTL.
+ *
+ * @param ttl requested duration in milliseconds to retain task from creation
+ * @return new TaskParameters instance
+ */
+ public static TaskParameters withTtl(Long ttl) {
+ return new TaskParameters(ttl);
+ }
+
+ /**
+ * Creates task parameters with default settings (no TTL specified).
+ *
+ * @return new TaskParameters instance with defaults
+ */
+ public static TaskParameters defaults() {
+ return new TaskParameters(null);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ TaskParameters that = (TaskParameters) o;
+ return Objects.equals(ttl, that.ttl);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(ttl);
+ }
+
+ @Override
+ public String toString() {
+ return "TaskParameters{" + "ttl=" + ttl + '}';
+ }
+}
diff --git a/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskStatus.java b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskStatus.java
new file mode 100644
index 000000000..cd2ce7452
--- /dev/null
+++ b/agentscope-core/src/main/java/io/agentscope/core/tool/mcp/task/TaskStatus.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * You may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+/**
+ * Enumeration of possible task statuses in the MCP protocol.
+ *
+ *
+ * Task statuses represent the current state of a task's execution lifecycle:
+ *
+ * {@link #WORKING} - The task is currently being processed
+ * {@link #INPUT_REQUIRED} - The task needs input from the requestor
+ * {@link #COMPLETED} - The task completed successfully
+ * {@link #FAILED} - The task failed to complete
+ * {@link #CANCELLED} - The task was cancelled before completion
+ *
+ *
+ * @see MCP
+ * Tasks Specification
+ */
+public enum TaskStatus {
+ /**
+ * The request is currently being processed.
+ */
+ WORKING("working"),
+
+ /**
+ * The receiver needs input from the requestor.
+ * The requestor should call tasks/result to receive input requests,
+ * even though the task has not reached a terminal state.
+ */
+ INPUT_REQUIRED("input_required"),
+
+ /**
+ * The request completed successfully and results are available.
+ */
+ COMPLETED("completed"),
+
+ /**
+ * The associated request did not complete successfully.
+ * For tool calls specifically, this includes cases where the tool call
+ * result has isError set to true.
+ */
+ FAILED("failed"),
+
+ /**
+ * The request was cancelled before completion.
+ */
+ CANCELLED("cancelled");
+
+ private final String value;
+
+ TaskStatus(String value) {
+ this.value = value;
+ }
+
+ /**
+ * Gets the string value of this status as used in the MCP protocol.
+ *
+ * @return the protocol string value
+ */
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * Parses a string value into a TaskStatus enum.
+ *
+ * @param value the string value to parse
+ * @return the corresponding TaskStatus
+ * @throws IllegalArgumentException if the value doesn't match any status
+ */
+ public static TaskStatus fromValue(String value) {
+ for (TaskStatus status : values()) {
+ if (status.value.equals(value)) {
+ return status;
+ }
+ }
+ throw new IllegalArgumentException("Unknown task status: " + value);
+ }
+
+ /**
+ * Checks if this status represents a terminal state.
+ * Terminal states are: COMPLETED, FAILED, and CANCELLED.
+ *
+ * @return true if this is a terminal status, false otherwise
+ */
+ public boolean isTerminal() {
+ return this == COMPLETED || this == FAILED || this == CANCELLED;
+ }
+
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpAsyncClientWrapperTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpAsyncClientWrapperTest.java
index 7167e65b3..fac566025 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpAsyncClientWrapperTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpAsyncClientWrapperTest.java
@@ -34,6 +34,7 @@
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
class McpAsyncClientWrapperTest {
@@ -228,4 +229,49 @@ private void setupSuccessfulInitialization() {
when(mockClient.initialize()).thenReturn(Mono.just(initResult));
when(mockClient.listTools()).thenReturn(Mono.just(toolsResult));
}
+
+ // TaskManager tests
+ @Test
+ void testGetTaskManager() {
+ assertNotNull(wrapper.getTaskManager());
+ }
+
+ @Test
+ void testGetTaskManager_ReturnsSameInstance() {
+ var taskManager1 = wrapper.getTaskManager();
+ var taskManager2 = wrapper.getTaskManager();
+ assertEquals(taskManager1, taskManager2);
+ }
+
+ @Test
+ void testTaskManager_GetTask_ThrowsUnsupportedOperation() {
+ var taskManager = wrapper.getTaskManager();
+ StepVerifier.create(taskManager.getTask("task-123"))
+ .expectError(UnsupportedOperationException.class)
+ .verify();
+ }
+
+ @Test
+ void testTaskManager_GetTaskResult_ThrowsUnsupportedOperation() {
+ var taskManager = wrapper.getTaskManager();
+ StepVerifier.create(taskManager.getTaskResult("task-123", Object.class))
+ .expectError(UnsupportedOperationException.class)
+ .verify();
+ }
+
+ @Test
+ void testTaskManager_ListTasks_ThrowsUnsupportedOperation() {
+ var taskManager = wrapper.getTaskManager();
+ StepVerifier.create(taskManager.listTasks(null))
+ .expectError(UnsupportedOperationException.class)
+ .verify();
+ }
+
+ @Test
+ void testTaskManager_CancelTask_ThrowsUnsupportedOperation() {
+ var taskManager = wrapper.getTaskManager();
+ StepVerifier.create(taskManager.cancelTask("task-123"))
+ .expectError(UnsupportedOperationException.class)
+ .verify();
+ }
}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientWrapperTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientWrapperTest.java
index a849d6eb4..2952d034b 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientWrapperTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpClientWrapperTest.java
@@ -21,6 +21,7 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import io.agentscope.core.tool.mcp.task.TaskManager;
import io.modelcontextprotocol.spec.McpSchema;
import java.util.List;
import java.util.Map;
@@ -234,6 +235,12 @@ public Mono callTool(
.build());
}
+ @Override
+ public TaskManager getTaskManager() {
+ // Return null for test purposes
+ return null;
+ }
+
@Override
public void close() {
initialized = false;
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpSyncClientWrapperTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpSyncClientWrapperTest.java
index 74b38eb7d..14ae07874 100644
--- a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpSyncClientWrapperTest.java
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/McpSyncClientWrapperTest.java
@@ -35,6 +35,7 @@
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import reactor.test.StepVerifier;
class McpSyncClientWrapperTest {
@@ -348,4 +349,49 @@ private void setupSuccessfulInitialization() {
when(mockClient.initialize()).thenReturn(initResult);
when(mockClient.listTools()).thenReturn(toolsResult);
}
+
+ // TaskManager tests
+ @Test
+ void testGetTaskManager() {
+ assertNotNull(wrapper.getTaskManager());
+ }
+
+ @Test
+ void testGetTaskManager_ReturnsSameInstance() {
+ var taskManager1 = wrapper.getTaskManager();
+ var taskManager2 = wrapper.getTaskManager();
+ assertEquals(taskManager1, taskManager2);
+ }
+
+ @Test
+ void testTaskManager_GetTask_ThrowsUnsupportedOperation() {
+ var taskManager = wrapper.getTaskManager();
+ StepVerifier.create(taskManager.getTask("task-123"))
+ .expectError(UnsupportedOperationException.class)
+ .verify();
+ }
+
+ @Test
+ void testTaskManager_GetTaskResult_ThrowsUnsupportedOperation() {
+ var taskManager = wrapper.getTaskManager();
+ StepVerifier.create(taskManager.getTaskResult("task-123", Object.class))
+ .expectError(UnsupportedOperationException.class)
+ .verify();
+ }
+
+ @Test
+ void testTaskManager_ListTasks_ThrowsUnsupportedOperation() {
+ var taskManager = wrapper.getTaskManager();
+ StepVerifier.create(taskManager.listTasks(null))
+ .expectError(UnsupportedOperationException.class)
+ .verify();
+ }
+
+ @Test
+ void testTaskManager_CancelTask_ThrowsUnsupportedOperation() {
+ var taskManager = wrapper.getTaskManager();
+ StepVerifier.create(taskManager.cancelTask("task-123"))
+ .expectError(UnsupportedOperationException.class)
+ .verify();
+ }
}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/DefaultTaskManagerTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/DefaultTaskManagerTest.java
new file mode 100644
index 000000000..e86eea87e
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/DefaultTaskManagerTest.java
@@ -0,0 +1,411 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+class DefaultTaskManagerTest {
+
+ private DefaultTaskManager taskManager;
+ private DefaultTaskManager.TaskOperations mockOperations;
+
+ private Task createTask(String taskId, TaskStatus status) {
+ return Task.builder()
+ .taskId(taskId)
+ .status(status)
+ .statusMessage("Test message")
+ .createdAt(Instant.now())
+ .lastUpdatedAt(Instant.now())
+ .ttl(60000L)
+ .pollInterval(5000L)
+ .build();
+ }
+
+ @BeforeEach
+ void setUp() {
+ mockOperations = mock(DefaultTaskManager.TaskOperations.class);
+ taskManager = new DefaultTaskManager("test-client", mockOperations);
+ }
+
+ @Test
+ void testGetTask_Success() {
+ Task expectedTask = createTask("task-123", TaskStatus.WORKING);
+ when(mockOperations.getTask("task-123")).thenReturn(Mono.just(expectedTask));
+
+ StepVerifier.create(taskManager.getTask("task-123"))
+ .expectNext(expectedTask)
+ .verifyComplete();
+
+ verify(mockOperations).getTask("task-123");
+ }
+
+ @Test
+ void testGetTask_CachesResult() {
+ Task task = createTask("task-123", TaskStatus.WORKING);
+ when(mockOperations.getTask("task-123")).thenReturn(Mono.just(task));
+
+ // First call
+ taskManager.getTask("task-123").block();
+
+ // Verify task is cached
+ assertEquals(task, taskManager.getCachedTask("task-123"));
+ }
+
+ @Test
+ void testGetTask_Error() {
+ when(mockOperations.getTask("task-123"))
+ .thenReturn(Mono.error(new RuntimeException("Task not found")));
+
+ StepVerifier.create(taskManager.getTask("task-123"))
+ .expectErrorMessage("Task not found")
+ .verify();
+ }
+
+ @Test
+ void testGetTaskResult_Success() {
+ Map resultData = new HashMap<>();
+ resultData.put("status", "success");
+ resultData.put("data", "test data");
+
+ when(mockOperations.getTaskResult("task-123")).thenReturn(Mono.just(resultData));
+
+ StepVerifier.create(taskManager.getTaskResult("task-123", Map.class))
+ .expectNext(resultData)
+ .verifyComplete();
+ }
+
+ @Test
+ void testGetTaskResult_TypeConversion() {
+ // Test JSON conversion
+ Map resultData = new HashMap<>();
+ resultData.put("message", "Hello");
+
+ when(mockOperations.getTaskResult("task-123")).thenReturn(Mono.just(resultData));
+
+ StepVerifier.create(taskManager.getTaskResult("task-123", Map.class))
+ .assertNext(
+ result -> {
+ assertNotNull(result);
+ assertEquals("Hello", result.get("message"));
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ void testGetTaskResult_ConversionError() {
+ when(mockOperations.getTaskResult("task-123")).thenReturn(Mono.just("invalid-object"));
+
+ StepVerifier.create(taskManager.getTaskResult("task-123", Integer.class))
+ .expectError(RuntimeException.class)
+ .verify();
+ }
+
+ @Test
+ void testListTasks_Success() {
+ List tasks =
+ Arrays.asList(
+ createTask("task-1", TaskStatus.WORKING),
+ createTask("task-2", TaskStatus.COMPLETED));
+ ListTasksResult result = new ListTasksResult(tasks, "next-cursor");
+
+ when(mockOperations.listTasks(null)).thenReturn(Mono.just(result));
+
+ StepVerifier.create(taskManager.listTasks(null))
+ .assertNext(
+ r -> {
+ assertEquals(2, r.getTasks().size());
+ assertEquals("next-cursor", r.getNextCursor());
+ })
+ .verifyComplete();
+ }
+
+ @Test
+ void testListTasks_CachesAllTasks() {
+ List tasks =
+ Arrays.asList(
+ createTask("task-1", TaskStatus.WORKING),
+ createTask("task-2", TaskStatus.COMPLETED));
+ ListTasksResult result = new ListTasksResult(tasks, null);
+
+ when(mockOperations.listTasks(null)).thenReturn(Mono.just(result));
+
+ taskManager.listTasks(null).block();
+
+ // Verify both tasks are cached
+ assertNotNull(taskManager.getCachedTask("task-1"));
+ assertNotNull(taskManager.getCachedTask("task-2"));
+ }
+
+ @Test
+ void testListTasks_WithCursor() {
+ List tasks = Arrays.asList(createTask("task-3", TaskStatus.WORKING));
+ ListTasksResult result = new ListTasksResult(tasks, null);
+
+ when(mockOperations.listTasks("cursor-123")).thenReturn(Mono.just(result));
+
+ StepVerifier.create(taskManager.listTasks("cursor-123"))
+ .assertNext(r -> assertEquals(1, r.getTasks().size()))
+ .verifyComplete();
+
+ verify(mockOperations).listTasks("cursor-123");
+ }
+
+ @Test
+ void testCancelTask_Success() {
+ Task cancelledTask = createTask("task-123", TaskStatus.CANCELLED);
+ when(mockOperations.cancelTask("task-123")).thenReturn(Mono.just(cancelledTask));
+
+ StepVerifier.create(taskManager.cancelTask("task-123"))
+ .assertNext(task -> assertEquals(TaskStatus.CANCELLED, task.getStatus()))
+ .verifyComplete();
+ }
+
+ @Test
+ void testCancelTask_UpdatesCache() {
+ Task cancelledTask = createTask("task-123", TaskStatus.CANCELLED);
+ when(mockOperations.cancelTask("task-123")).thenReturn(Mono.just(cancelledTask));
+
+ taskManager.cancelTask("task-123").block();
+
+ Task cachedTask = taskManager.getCachedTask("task-123");
+ assertNotNull(cachedTask);
+ assertEquals(TaskStatus.CANCELLED, cachedTask.getStatus());
+ }
+
+ @Test
+ void testRegisterTaskStatusListener() {
+ AtomicInteger callCount = new AtomicInteger(0);
+ TaskManager.TaskStatusListener listener = task -> callCount.incrementAndGet();
+
+ taskManager.registerTaskStatusListener(listener);
+
+ // Trigger a notification
+ Task task = createTask("task-123", TaskStatus.COMPLETED);
+ taskManager.handleTaskStatusNotification(task);
+
+ assertEquals(1, callCount.get());
+ }
+
+ @Test
+ void testRegisterTaskStatusListener_MultipleListeners() {
+ AtomicInteger callCount1 = new AtomicInteger(0);
+ AtomicInteger callCount2 = new AtomicInteger(0);
+
+ TaskManager.TaskStatusListener listener1 = task -> callCount1.incrementAndGet();
+ TaskManager.TaskStatusListener listener2 = task -> callCount2.incrementAndGet();
+
+ taskManager.registerTaskStatusListener(listener1);
+ taskManager.registerTaskStatusListener(listener2);
+
+ Task task = createTask("task-123", TaskStatus.COMPLETED);
+ taskManager.handleTaskStatusNotification(task);
+
+ assertEquals(1, callCount1.get());
+ assertEquals(1, callCount2.get());
+ }
+
+ @Test
+ void testRegisterTaskStatusListener_NullListener() {
+ // Should not throw exception
+ taskManager.registerTaskStatusListener(null);
+
+ Task task = createTask("task-123", TaskStatus.COMPLETED);
+ taskManager.handleTaskStatusNotification(task);
+ }
+
+ @Test
+ void testRegisterTaskStatusListener_DuplicateListener() {
+ AtomicInteger callCount = new AtomicInteger(0);
+ TaskManager.TaskStatusListener listener = task -> callCount.incrementAndGet();
+
+ taskManager.registerTaskStatusListener(listener);
+ taskManager.registerTaskStatusListener(listener); // Register twice
+
+ Task task = createTask("task-123", TaskStatus.COMPLETED);
+ taskManager.handleTaskStatusNotification(task);
+
+ // Should only be called once (no duplicates)
+ assertEquals(1, callCount.get());
+ }
+
+ @Test
+ void testUnregisterTaskStatusListener() {
+ AtomicInteger callCount = new AtomicInteger(0);
+ TaskManager.TaskStatusListener listener = task -> callCount.incrementAndGet();
+
+ taskManager.registerTaskStatusListener(listener);
+ taskManager.unregisterTaskStatusListener(listener);
+
+ Task task = createTask("task-123", TaskStatus.COMPLETED);
+ taskManager.handleTaskStatusNotification(task);
+
+ assertEquals(0, callCount.get());
+ }
+
+ @Test
+ void testUnregisterTaskStatusListener_NotRegistered() {
+ TaskManager.TaskStatusListener listener = task -> {};
+
+ // Should not throw exception
+ taskManager.unregisterTaskStatusListener(listener);
+ }
+
+ @Test
+ void testHandleTaskStatusNotification_UpdatesCache() {
+ Task task = createTask("task-123", TaskStatus.COMPLETED);
+
+ taskManager.handleTaskStatusNotification(task);
+
+ assertEquals(task, taskManager.getCachedTask("task-123"));
+ }
+
+ @Test
+ void testHandleTaskStatusNotification_NotifiesListeners() {
+ AtomicInteger callCount = new AtomicInteger(0);
+ TaskManager.TaskStatusListener listener = task -> callCount.incrementAndGet();
+
+ taskManager.registerTaskStatusListener(listener);
+
+ Task task1 = createTask("task-1", TaskStatus.WORKING);
+ Task task2 = createTask("task-2", TaskStatus.COMPLETED);
+
+ taskManager.handleTaskStatusNotification(task1);
+ taskManager.handleTaskStatusNotification(task2);
+
+ assertEquals(2, callCount.get());
+ }
+
+ @Test
+ void testHandleTaskStatusNotification_ListenerException() {
+ TaskManager.TaskStatusListener faultyListener =
+ task -> {
+ throw new RuntimeException("Listener error");
+ };
+
+ AtomicInteger callCount = new AtomicInteger(0);
+ TaskManager.TaskStatusListener goodListener = task -> callCount.incrementAndGet();
+
+ taskManager.registerTaskStatusListener(faultyListener);
+ taskManager.registerTaskStatusListener(goodListener);
+
+ Task task = createTask("task-123", TaskStatus.COMPLETED);
+
+ // Should not throw exception, and good listener should still be called
+ taskManager.handleTaskStatusNotification(task);
+
+ assertEquals(1, callCount.get());
+ }
+
+ @Test
+ void testGetCachedTask_NotFound() {
+ assertNull(taskManager.getCachedTask("non-existent"));
+ }
+
+ @Test
+ void testGetAllCachedTasks() {
+ Task task1 = createTask("task-1", TaskStatus.WORKING);
+ Task task2 = createTask("task-2", TaskStatus.COMPLETED);
+
+ taskManager.handleTaskStatusNotification(task1);
+ taskManager.handleTaskStatusNotification(task2);
+
+ List allTasks = taskManager.getAllCachedTasks();
+
+ assertEquals(2, allTasks.size());
+ assertTrue(allTasks.contains(task1));
+ assertTrue(allTasks.contains(task2));
+ }
+
+ @Test
+ void testClearCache() {
+ Task task = createTask("task-123", TaskStatus.WORKING);
+ taskManager.handleTaskStatusNotification(task);
+
+ assertNotNull(taskManager.getCachedTask("task-123"));
+
+ taskManager.clearCache();
+
+ assertNull(taskManager.getCachedTask("task-123"));
+ assertTrue(taskManager.getAllCachedTasks().isEmpty());
+ }
+
+ @Test
+ void testCreateTask_UtilityMethod() {
+ Task task =
+ DefaultTaskManager.createTask(
+ "task-123",
+ "working",
+ "Processing",
+ "2025-12-17T10:00:00Z",
+ "2025-12-17T10:05:00Z",
+ 60000L,
+ 5000L);
+
+ assertEquals("task-123", task.getTaskId());
+ assertEquals(TaskStatus.WORKING, task.getStatus());
+ assertEquals("Processing", task.getStatusMessage());
+ assertEquals(60000L, task.getTtl());
+ assertEquals(5000L, task.getPollInterval());
+ }
+
+ @Test
+ void testCreateTask_InvalidStatus() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () ->
+ DefaultTaskManager.createTask(
+ "task-123",
+ "invalid-status",
+ null,
+ "2025-12-17T10:00:00Z",
+ "2025-12-17T10:05:00Z",
+ null,
+ null));
+ }
+
+ @Test
+ void testCreateTask_InvalidTimestamp() {
+ assertThrows(
+ Exception.class,
+ () ->
+ DefaultTaskManager.createTask(
+ "task-123",
+ "working",
+ null,
+ "invalid-timestamp",
+ "2025-12-17T10:05:00Z",
+ null,
+ null));
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/ListTasksResultTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/ListTasksResultTest.java
new file mode 100644
index 000000000..452505a5c
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/ListTasksResultTest.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class ListTasksResultTest {
+
+ private Task createTask(String taskId, TaskStatus status) {
+ return Task.builder()
+ .taskId(taskId)
+ .status(status)
+ .createdAt(Instant.now())
+ .lastUpdatedAt(Instant.now())
+ .build();
+ }
+
+ @Test
+ void testConstructor_WithTasksAndCursor() {
+ List tasks =
+ Arrays.asList(
+ createTask("task-1", TaskStatus.WORKING),
+ createTask("task-2", TaskStatus.COMPLETED));
+ String cursor = "next-page-cursor";
+
+ ListTasksResult result = new ListTasksResult(tasks, cursor);
+
+ assertEquals(tasks, result.getTasks());
+ assertEquals(cursor, result.getNextCursor());
+ }
+
+ @Test
+ void testConstructor_WithTasksNoCursor() {
+ List tasks = Arrays.asList(createTask("task-1", TaskStatus.WORKING));
+
+ ListTasksResult result = new ListTasksResult(tasks, null);
+
+ assertEquals(tasks, result.getTasks());
+ assertNull(result.getNextCursor());
+ }
+
+ @Test
+ void testConstructor_EmptyTasks() {
+ List tasks = Collections.emptyList();
+
+ ListTasksResult result = new ListTasksResult(tasks, null);
+
+ assertTrue(result.getTasks().isEmpty());
+ assertNull(result.getNextCursor());
+ }
+
+ @Test
+ void testConstructor_NullTasks() {
+ assertThrows(NullPointerException.class, () -> new ListTasksResult(null, "cursor"));
+ }
+
+ @Test
+ void testHasMore_WithCursor() {
+ List tasks = Arrays.asList(createTask("task-1", TaskStatus.WORKING));
+ ListTasksResult result = new ListTasksResult(tasks, "next-cursor");
+
+ assertTrue(result.hasMore());
+ }
+
+ @Test
+ void testHasMore_NullCursor() {
+ List tasks = Arrays.asList(createTask("task-1", TaskStatus.WORKING));
+ ListTasksResult result = new ListTasksResult(tasks, null);
+
+ assertFalse(result.hasMore());
+ }
+
+ @Test
+ void testHasMore_EmptyCursor() {
+ List tasks = Arrays.asList(createTask("task-1", TaskStatus.WORKING));
+ ListTasksResult result = new ListTasksResult(tasks, "");
+
+ assertFalse(result.hasMore());
+ }
+
+ @Test
+ void testEquals_SameResults() {
+ List tasks = Arrays.asList(createTask("task-1", TaskStatus.WORKING));
+ ListTasksResult result1 = new ListTasksResult(tasks, "cursor");
+ ListTasksResult result2 = new ListTasksResult(tasks, "cursor");
+
+ assertEquals(result1, result2);
+ assertEquals(result1.hashCode(), result2.hashCode());
+ }
+
+ @Test
+ void testEquals_DifferentCursor() {
+ List tasks = Arrays.asList(createTask("task-1", TaskStatus.WORKING));
+ ListTasksResult result1 = new ListTasksResult(tasks, "cursor1");
+ ListTasksResult result2 = new ListTasksResult(tasks, "cursor2");
+
+ assertNotEquals(result1, result2);
+ }
+
+ @Test
+ void testEquals_DifferentTasks() {
+ ListTasksResult result1 =
+ new ListTasksResult(
+ Arrays.asList(createTask("task-1", TaskStatus.WORKING)), "cursor");
+ ListTasksResult result2 =
+ new ListTasksResult(
+ Arrays.asList(createTask("task-2", TaskStatus.COMPLETED)), "cursor");
+
+ assertNotEquals(result1, result2);
+ }
+
+ @Test
+ void testToString_ContainsTasksAndCursor() {
+ List tasks = Arrays.asList(createTask("task-1", TaskStatus.WORKING));
+ String cursor = "next-cursor";
+ ListTasksResult result = new ListTasksResult(tasks, cursor);
+
+ String toString = result.toString();
+ assertTrue(toString.contains("tasks"));
+ assertTrue(toString.contains(cursor));
+ }
+
+ @Test
+ void testGetTasks_ReturnsUnmodifiableList() {
+ List tasks = Arrays.asList(createTask("task-1", TaskStatus.WORKING));
+ ListTasksResult result = new ListTasksResult(tasks, null);
+
+ List returnedTasks = result.getTasks();
+
+ // Verify the list contains the same elements
+ assertEquals(tasks, returnedTasks);
+
+ // Verify the list is unmodifiable
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> {
+ returnedTasks.add(createTask("task-2", TaskStatus.COMPLETED));
+ });
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskParametersTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskParametersTest.java
new file mode 100644
index 000000000..968d36bb6
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskParametersTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class TaskParametersTest {
+
+ @Test
+ void testConstructor_WithTtl() {
+ Long ttl = 60000L;
+ TaskParameters params = new TaskParameters(ttl);
+
+ assertEquals(ttl, params.getTtl());
+ }
+
+ @Test
+ void testConstructor_NullTtl() {
+ TaskParameters params = new TaskParameters(null);
+
+ assertNull(params.getTtl());
+ }
+
+ @Test
+ void testWithTtl_FactoryMethod() {
+ Long ttl = 30000L;
+ TaskParameters params = TaskParameters.withTtl(ttl);
+
+ assertEquals(ttl, params.getTtl());
+ }
+
+ @Test
+ void testDefaults_FactoryMethod() {
+ TaskParameters params = TaskParameters.defaults();
+
+ assertNull(params.getTtl());
+ }
+
+ @Test
+ void testEquals_SameParameters() {
+ TaskParameters params1 = TaskParameters.withTtl(60000L);
+ TaskParameters params2 = TaskParameters.withTtl(60000L);
+
+ assertEquals(params1, params2);
+ assertEquals(params1.hashCode(), params2.hashCode());
+ }
+
+ @Test
+ void testEquals_DifferentTtl() {
+ TaskParameters params1 = TaskParameters.withTtl(60000L);
+ TaskParameters params2 = TaskParameters.withTtl(30000L);
+
+ assertNotEquals(params1, params2);
+ }
+
+ @Test
+ void testEquals_BothNull() {
+ TaskParameters params1 = TaskParameters.defaults();
+ TaskParameters params2 = TaskParameters.defaults();
+
+ assertEquals(params1, params2);
+ assertEquals(params1.hashCode(), params2.hashCode());
+ }
+
+ @Test
+ void testEquals_OneNullOneTtl() {
+ TaskParameters params1 = TaskParameters.defaults();
+ TaskParameters params2 = TaskParameters.withTtl(60000L);
+
+ assertNotEquals(params1, params2);
+ }
+
+ @Test
+ void testToString_WithTtl() {
+ TaskParameters params = TaskParameters.withTtl(60000L);
+ String toString = params.toString();
+
+ assertTrue(toString.contains("60000"));
+ }
+
+ @Test
+ void testToString_NullTtl() {
+ TaskParameters params = TaskParameters.defaults();
+ String toString = params.toString();
+
+ assertTrue(toString.contains("null"));
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskStatusTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskStatusTest.java
new file mode 100644
index 000000000..a26acd454
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskStatusTest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class TaskStatusTest {
+
+ @Test
+ void testGetValue() {
+ assertEquals("working", TaskStatus.WORKING.getValue());
+ assertEquals("input_required", TaskStatus.INPUT_REQUIRED.getValue());
+ assertEquals("completed", TaskStatus.COMPLETED.getValue());
+ assertEquals("failed", TaskStatus.FAILED.getValue());
+ assertEquals("cancelled", TaskStatus.CANCELLED.getValue());
+ }
+
+ @Test
+ void testFromValue_ValidValues() {
+ assertEquals(TaskStatus.WORKING, TaskStatus.fromValue("working"));
+ assertEquals(TaskStatus.INPUT_REQUIRED, TaskStatus.fromValue("input_required"));
+ assertEquals(TaskStatus.COMPLETED, TaskStatus.fromValue("completed"));
+ assertEquals(TaskStatus.FAILED, TaskStatus.fromValue("failed"));
+ assertEquals(TaskStatus.CANCELLED, TaskStatus.fromValue("cancelled"));
+ }
+
+ @Test
+ void testFromValue_InvalidValue() {
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> TaskStatus.fromValue("invalid_status"),
+ "Should throw IllegalArgumentException for unknown status");
+ }
+
+ @Test
+ void testFromValue_NullValue() {
+ // When value is null, the for loop doesn't match anything and throws
+ // IllegalArgumentException
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> TaskStatus.fromValue(null),
+ "Should throw IllegalArgumentException for null value");
+ }
+
+ @Test
+ void testIsTerminal_TerminalStates() {
+ assertTrue(TaskStatus.COMPLETED.isTerminal(), "COMPLETED should be terminal");
+ assertTrue(TaskStatus.FAILED.isTerminal(), "FAILED should be terminal");
+ assertTrue(TaskStatus.CANCELLED.isTerminal(), "CANCELLED should be terminal");
+ }
+
+ @Test
+ void testIsTerminal_NonTerminalStates() {
+ assertFalse(TaskStatus.WORKING.isTerminal(), "WORKING should not be terminal");
+ assertFalse(
+ TaskStatus.INPUT_REQUIRED.isTerminal(), "INPUT_REQUIRED should not be terminal");
+ }
+
+ @Test
+ void testToString() {
+ assertEquals("working", TaskStatus.WORKING.toString());
+ assertEquals("input_required", TaskStatus.INPUT_REQUIRED.toString());
+ assertEquals("completed", TaskStatus.COMPLETED.toString());
+ assertEquals("failed", TaskStatus.FAILED.toString());
+ assertEquals("cancelled", TaskStatus.CANCELLED.toString());
+ }
+
+ @Test
+ void testAllValuesHaveUniqueStrings() {
+ TaskStatus[] values = TaskStatus.values();
+ for (int i = 0; i < values.length; i++) {
+ for (int j = i + 1; j < values.length; j++) {
+ assertNotEquals(
+ values[i].getValue(),
+ values[j].getValue(),
+ "Each status should have a unique value");
+ }
+ }
+ }
+
+ @Test
+ void testRoundTrip() {
+ // Test that fromValue(getValue()) returns the same enum
+ for (TaskStatus status : TaskStatus.values()) {
+ assertEquals(status, TaskStatus.fromValue(status.getValue()));
+ }
+ }
+}
diff --git a/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskTest.java b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskTest.java
new file mode 100644
index 000000000..09bfbfa40
--- /dev/null
+++ b/agentscope-core/src/test/java/io/agentscope/core/tool/mcp/task/TaskTest.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2024-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.tool.mcp.task;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.time.Instant;
+import org.junit.jupiter.api.Test;
+
+class TaskTest {
+
+ private static final String TASK_ID = "test-task-123";
+ private static final TaskStatus STATUS = TaskStatus.WORKING;
+ private static final String STATUS_MESSAGE = "Processing request";
+ private static final Instant CREATED_AT = Instant.parse("2025-12-17T10:00:00Z");
+ private static final Instant UPDATED_AT = Instant.parse("2025-12-17T10:05:00Z");
+ private static final Long TTL = 60000L;
+ private static final Long POLL_INTERVAL = 5000L;
+
+ @Test
+ void testBuilder_AllFields() {
+ Task task =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(STATUS)
+ .statusMessage(STATUS_MESSAGE)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .ttl(TTL)
+ .pollInterval(POLL_INTERVAL)
+ .build();
+
+ assertEquals(TASK_ID, task.getTaskId());
+ assertEquals(STATUS, task.getStatus());
+ assertEquals(STATUS_MESSAGE, task.getStatusMessage());
+ assertEquals(CREATED_AT, task.getCreatedAt());
+ assertEquals(UPDATED_AT, task.getLastUpdatedAt());
+ assertEquals(TTL, task.getTtl());
+ assertEquals(POLL_INTERVAL, task.getPollInterval());
+ }
+
+ @Test
+ void testBuilder_MinimalFields() {
+ Task task =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(STATUS)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ assertEquals(TASK_ID, task.getTaskId());
+ assertEquals(STATUS, task.getStatus());
+ assertNull(task.getStatusMessage());
+ assertEquals(CREATED_AT, task.getCreatedAt());
+ assertEquals(UPDATED_AT, task.getLastUpdatedAt());
+ assertNull(task.getTtl());
+ assertNull(task.getPollInterval());
+ }
+
+ @Test
+ void testConstructor_NullTaskId() {
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ new Task(
+ null,
+ STATUS,
+ STATUS_MESSAGE,
+ CREATED_AT,
+ UPDATED_AT,
+ TTL,
+ POLL_INTERVAL));
+ }
+
+ @Test
+ void testConstructor_NullStatus() {
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ new Task(
+ TASK_ID,
+ null,
+ STATUS_MESSAGE,
+ CREATED_AT,
+ UPDATED_AT,
+ TTL,
+ POLL_INTERVAL));
+ }
+
+ @Test
+ void testConstructor_NullCreatedAt() {
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ new Task(
+ TASK_ID,
+ STATUS,
+ STATUS_MESSAGE,
+ null,
+ UPDATED_AT,
+ TTL,
+ POLL_INTERVAL));
+ }
+
+ @Test
+ void testConstructor_NullLastUpdatedAt() {
+ assertThrows(
+ NullPointerException.class,
+ () ->
+ new Task(
+ TASK_ID,
+ STATUS,
+ STATUS_MESSAGE,
+ CREATED_AT,
+ null,
+ TTL,
+ POLL_INTERVAL));
+ }
+
+ @Test
+ void testIsTerminal_CompletedTask() {
+ Task task =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(TaskStatus.COMPLETED)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ assertTrue(task.isTerminal());
+ }
+
+ @Test
+ void testIsTerminal_FailedTask() {
+ Task task =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(TaskStatus.FAILED)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ assertTrue(task.isTerminal());
+ }
+
+ @Test
+ void testIsTerminal_CancelledTask() {
+ Task task =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(TaskStatus.CANCELLED)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ assertTrue(task.isTerminal());
+ }
+
+ @Test
+ void testIsTerminal_WorkingTask() {
+ Task task =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(TaskStatus.WORKING)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ assertFalse(task.isTerminal());
+ }
+
+ @Test
+ void testEquals_SameTasks() {
+ Task task1 =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(STATUS)
+ .statusMessage(STATUS_MESSAGE)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .ttl(TTL)
+ .pollInterval(POLL_INTERVAL)
+ .build();
+
+ Task task2 =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(STATUS)
+ .statusMessage(STATUS_MESSAGE)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .ttl(TTL)
+ .pollInterval(POLL_INTERVAL)
+ .build();
+
+ assertEquals(task1, task2);
+ assertEquals(task1.hashCode(), task2.hashCode());
+ }
+
+ @Test
+ void testEquals_DifferentTaskId() {
+ Task task1 =
+ Task.builder()
+ .taskId("task-1")
+ .status(STATUS)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ Task task2 =
+ Task.builder()
+ .taskId("task-2")
+ .status(STATUS)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ assertNotEquals(task1, task2);
+ }
+
+ @Test
+ void testEquals_DifferentStatus() {
+ Task task1 =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(TaskStatus.WORKING)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ Task task2 =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(TaskStatus.COMPLETED)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .build();
+
+ assertNotEquals(task1, task2);
+ }
+
+ @Test
+ void testToString_ContainsAllFields() {
+ Task task =
+ Task.builder()
+ .taskId(TASK_ID)
+ .status(STATUS)
+ .statusMessage(STATUS_MESSAGE)
+ .createdAt(CREATED_AT)
+ .lastUpdatedAt(UPDATED_AT)
+ .ttl(TTL)
+ .pollInterval(POLL_INTERVAL)
+ .build();
+
+ String toString = task.toString();
+ assertTrue(toString.contains(TASK_ID));
+ assertTrue(toString.contains(STATUS.toString()));
+ assertTrue(toString.contains(STATUS_MESSAGE));
+ }
+}
diff --git a/agentscope-dependencies-bom/pom.xml b/agentscope-dependencies-bom/pom.xml
index 0cc958255..4bdc68ba8 100644
--- a/agentscope-dependencies-bom/pom.xml
+++ b/agentscope-dependencies-bom/pom.xml
@@ -134,7 +134,7 @@
io.modelcontextprotocol.sdk
mcp
- 0.14.1
+ 0.17.0