Skip to content

Commit e2c1762

Browse files
authored
feat(server): implement task-augmented tools capability (#707)
* feat(mcp): add ToolExecution types and TaskSupport constants Add foundational types for task-augmented tool support per MCP spec 2025-11-25: - Add TaskSupport type (string enum) with three constants: - TaskSupportForbidden: tool cannot be invoked as a task (default) - TaskSupportOptional: tool can be invoked either way - TaskSupportRequired: tool must be invoked as a task - Add ToolExecution struct with TaskSupport field - Update Tool struct to include optional Execution field - Update Tool.MarshalJSON to serialize execution field when present - Add comprehensive tests for marshaling behavior and constants Related to #656 * feat(tasks): implement pagination for tasks/list endpoint - Add Task.GetName() method to implement Named interface for pagination - Update handleListTasks to sort tasks by TaskId and use listByPagination - Add comprehensive pagination tests covering multiple pages and edge cases - Add TestTask_GetName unit test All tests pass with race detector, linter clean. * feat(tasks): add RelatedTaskMeta helper functions Add helper functions for associating messages with tasks per MCP spec: - RelatedTaskMetaKey constant for standard metadata key - RelatedTaskMeta() to create task metadata maps - WithRelatedTask() to create Meta objects with task IDs Includes comprehensive test coverage in mcp/tasks_test.go. Part of #656 (TAS-14) * feat(tasks): add WithTaskSupport option for tool configuration Add WithTaskSupport() function to configure task support modes for tools. The function accepts TaskSupportForbidden, TaskSupportOptional, or TaskSupportRequired values and properly initializes or updates the tool's Execution field. - Implement WithTaskSupport() in mcp/tools.go - Add comprehensive test coverage in mcp/tools_test.go - Test all three task support modes with JSON marshaling - Test Execution initialization when nil - Test preservation of existing Execution instance Related to #656 * feat(tasks): add TaskParams field to CallToolParams for task-augmented tool calls - Add Task *TaskParams field to CallToolParams struct to support task augmentation - Add ToInt64Ptr() helper function in utils.go for creating int64 pointers - Add comprehensive tests for CallToolRequest with task params: - Test unmarshaling with task params, without task params, and null task params - Test round-trip marshaling/unmarshaling to verify data preservation - All tests pass with race detector, linter clean This enables detection of task-augmented tool calls and unblocks: - TAS-15: Support for optional task tools (hybrid mode) - TAS-16: Validation of task support on required tools Related to #656 * feat(tasks): add lastUpdatedAt field to Task type Add LastUpdatedAt field to Task struct per MCP spec 2025-11-25 requirement. The field is automatically set on task creation and updated whenever the task status changes (completed, failed, or cancelled). Changes: - Add LastUpdatedAt string field to Task struct in mcp/types.go - Initialize both CreatedAt and LastUpdatedAt in NewTask() (mcp/tasks.go) - Update lastUpdatedAt in completeTask() when task completes or fails - Update lastUpdatedAt in cancelTask() when task is cancelled - Add comprehensive test coverage for timestamp behavior - Enhance JSON marshaling tests to verify field presence All tests pass, linter clean. Complies with MCP spec requirement that tasks MUST include lastUpdatedAt ISO 8601 timestamp. * feat(tasks): add validation for TaskSupportRequired tools Implement validation in handleToolCall() to enforce that tools with TaskSupportRequired cannot be called without task augmentation. Returns METHOD_NOT_FOUND error with message 'tool X requires task augmentation'. Add comprehensive test suite TestMCPServer_TaskSupportValidation with 7 test cases covering all task support modes (required, optional, forbidden). Implements MCP spec 2025-11-25 requirement for task support validation. Related to #656 * feat(tasks): add task status notifications Implement notifications/tasks/status to notify clients when task status changes. Notifications are sent automatically when tasks complete, fail, or are cancelled. Changes: - Add sendTaskStatusNotification() helper method that converts task data to notification params format - Include required fields: taskId, status, createdAt, lastUpdatedAt - Conditionally include optional fields: statusMessage, ttl, pollInterval - Integrate notifications into completeTask() and cancelTask() - Add comprehensive test suite with 7 test cases covering all scenarios Complies with MCP spec 2025-11-25 task notification requirements. Issue: #656 * feat(tasks): implement hybrid mode detection for optional task tools Add routing logic to detect when tools with TaskSupportOptional or TaskSupportRequired are called with task parameters. Tools called with task params now route to task-augmented execution path (placeholder error until TAS-9 implements handleTaskAugmentedToolCall). Tools called without task params continue to execute synchronously. - Add shouldExecuteAsTask detection in handleToolCall() - Add comprehensive test suite TestMCPServer_HybridModeDetection - Update existing tests to expect task execution routing behavior This provides the routing foundation for TAS-9 to implement actual task-augmented tool execution. * feat(server): add task-augmented tool call detection and routing - Add detection logic in handleToolCall() to identify task-augmented requests - Route requests with task params to handleTaskAugmentedToolCall() when tool has TaskSupportOptional or TaskSupportRequired - Add handleTaskAugmentedToolCall() stub method with TODO for TAS-9 implementation - Completes TAS-8: routing infrastructure for task-augmented tool execution Related to #656 * feat(examples): add task tool example demonstrating async execution Add comprehensive example showing task-augmented tools with three types: - process_batch: TaskSupportRequired tool for batch processing - analyze_data: TaskSupportOptional tool with sync/async modes - quick_check: Regular synchronous tool for comparison Includes detailed README with usage examples, HTTP testing instructions, and task workflow explanation. Build constraint prevents compilation until core implementation (TAS-4 through TAS-10) is complete. Related to #656 (TAS-19) * feat(server): add TaskToolHandlerFunc type and ServerTaskTool struct for async task execution - Add TaskToolHandlerFunc type for asynchronous tool call handlers that return CreateTaskResult - Add ServerTaskTool struct combining Tool with TaskToolHandlerFunc - Add taskTools map to MCPServer for storing task-augmented tools - Initialize taskTools map in NewMCPServer constructor - Add comprehensive test suite validating type definitions and initialization These type definitions provide the foundation for task-augmented tool execution (TAS-4, TAS-5). Related to #656 * feat(tasks): implement executeTaskTool method for async task execution Implement executeTaskTool method that executes task tool handlers asynchronously. The method creates a cancellable context, stores the cancel function for potential cancellation via tasks/cancel, executes the handler in the background, and completes the task with result or error. Implementation details: - Creates cancellable context using context.WithCancel() - Stores cancel func in task entry under lock before execution - Executes TaskToolHandlerFunc and handles results/errors - Calls completeTask() to update status and send notifications Test coverage includes 6 comprehensive test cases: - Successful task execution stores result - Failed task execution stores error with proper status - Task cancellation via context - Cancel function storage timing (race condition test) - Task status notification on completion - Multiple concurrent task execution All tests pass with race detector, linter clean. This provides the core async execution mechanism for TAS-9 (handleTaskAugmentedToolCall). Related to #656 * feat(server): implement task-augmented tool call handling (TAS-9) Implement handleTaskAugmentedToolCall method that enables async execution of tools with TaskSupport. The implementation: - Checks both taskTools and regular tools maps for tool lookup - Validates task support mode (optional/required) - Generates UUID v4 task IDs and creates task entries - Starts async execution via executeTaskTool or executeRegularToolAsTask - Returns CreateTaskResult immediately wrapped in CallToolResult _meta field - Fixes race condition by copying task before returning Add executeRegularToolAsTask method to handle hybrid mode where regular tools with TaskSupport are executed asynchronously. Update test expectations in TestMCPServer_HybridModeDetection and TestMCPServer_TaskSupportValidation to expect successful task creation instead of 'not yet implemented' error. Tools with TaskSupportOptional/Required can now be called with task params and execute asynchronously with proper task tracking. * docs: add comprehensive task-augmented tools documentation to README - Add Task-Augmented Tools section explaining async execution - Document three TaskSupport modes (Forbidden, Optional, Required) - Include code examples for TaskSupportRequired and TaskSupportOptional - Explain task execution flow (call → task ID → async → polling → notifications) - Add context cancellation and WithTaskCapabilities examples - Update Examples section to highlight task_tool example - Reorganize synchronous tools section for clarity * feat(server): include task tools in handleListTools response Update handleListTools to return both regular and task-augmented tools in tools/list response. Implementation iterates over both s.tools and s.taskTools maps, sorts all tool names alphabetically, and includes both types in the result. - Extend capacity of tools slice to accommodate both tool types - Collect tool names from both s.tools and s.taskTools maps - Maintain alphabetical sorting across all tools - Protected by existing toolsMu mutex for thread safety - Add comprehensive test suite with 4 test cases: * Mixed regular and task tools with proper sorting * Only task tools scenario * Empty list when no tools exist * Alphabetical ordering verification Task tools identified by Execution.TaskSupport field in metadata. * feat(server): add AddTaskTool and AddTaskTools methods Implement proper API for registering task-augmented tools following the same pattern as regular tool registration (AddTool/AddTools). - Add AddTaskTool() method for single task tool registration - Add AddTaskTools() method for batch task tool registration - Both methods implicitly register tool capabilities (listChanged=true) - Send tools/list_changed notifications to initialized sessions - Add comprehensive test suite TestMCPServer_AddTaskTool with 4 test cases - Update existing tests to use new methods instead of direct map access All tests pass with race detector, linter clean (0 issues). * test: add comprehensive task tool integration test (TAS-12) Add TestTaskToolTracerBullet in server/task_tool_test.go with 6 end-to-end test scenarios covering: - Complete task tool flow with TaskSupportRequired - TaskSupportOptional sync/async execution modes - Task cancellation via context - Error handling with proper task failure states - Multiple concurrent task tools Fix bug in handleToolCall where taskTools map was not checked during tool lookup. Now checks both s.tools and s.taskTools, allowing tools registered with AddTaskTool to be properly found and validated. All tests pass with race detector, linter reports 0 issues. * feat(server): improve context cancellation handling for task tools - Add explicit checking for context.Canceled and context.DeadlineExceeded errors in executeTaskTool and executeRegularToolAsTask - Mark tasks as 'cancelled' instead of 'failed' when handlers return context cancellation errors - Handle race condition where handler detects cancellation before tasks/cancel is called - Add comprehensive test for handler-detected context cancellation edge case - Update existing test expectations to correctly expect TaskStatusCancelled This ensures proper status differentiation between cancellation and failure, respecting Go's context patterns. * feat(server): add task metrics and observability hooks Implement comprehensive observability system for task-augmented tools: - Add TaskMetrics struct with execution metadata (ID, tool name, status, timestamps, duration, session, error) - Add TaskHooks with 5 lifecycle hook types: OnTaskCreated, OnTaskCompleted, OnTaskFailed, OnTaskCancelled, OnTaskStatusChanged - Add WithTaskHooks() ServerOption for hook configuration - Integrate hooks into task lifecycle (createTask, completeTask, cancelTask, executeTaskTool, executeRegularToolAsTask) - Update taskEntry to track toolName and createdAt for metrics - Add comprehensive test suite with 8 test cases covering all hook scenarios - Update task_tool example to demonstrate hook usage with logging This enables developers to monitor task execution, collect metrics, and integrate with monitoring systems for async tool operations. Resolves TAS-24 * feat(server): add max concurrent tasks limit for task-augmented tools Add optional resource management for task-augmented tools through WithMaxConcurrentTasks() ServerOption. This prevents resource exhaustion by limiting the number of concurrent running tasks. Changes: - Add maxConcurrentTasks and activeTasks fields to MCPServer - Add WithMaxConcurrentTasks(limit int) ServerOption - Modify createTask() to check limit and return error when exceeded - Update task completion/cancellation to decrement active task counter - Add comprehensive test suite (8 test cases) with race detector - Update examples and documentation The limit only applies to non-terminal (working) tasks. Completed, failed, or cancelled tasks don't count toward the limit. When not configured or set to 0, no limit is enforced. All tests pass with race detector. Linter clean. * types * remove * fix(tasks): eliminate race condition in createTask and add tool name collision validation - Fix race window in createTask() between checking concurrent task limit and inserting task - Restructure to build task entry first without lock - Use single critical section with defer for check + increment + insert - Prevents multiple goroutines from exceeding maxConcurrentTasks under high concurrency - Add tool name collision validation - Prevent same tool name from being registered in both s.tools and s.taskTools - AddTools() now checks for collisions with s.taskTools before registration - AddTaskTools() now checks for collisions with s.tools before registration - Panic with clear error message on collision to fail fast during setup - Prevents silent shadowing where task tools become invisible in handleListTools - Add comprehensive test coverage - Test concurrent task creation respects limit (existing test now passes reliably) - Add TestMCPServer_ToolNameCollisionValidation with 5 collision scenarios - All tests pass with race detector enabled * fix(tasks): add mutex synchronization to eliminate data races in tests Shared variables handlerCalled and receivedData were accessed without synchronization from both the async task handler goroutine and the main test thread, causing data races detected by 'go test -race'. Added sync.Mutex protection around all reads and writes to these variables. All tests now pass with race detector enabled. * fix: return CreateTaskResult with task as direct field for spec compliance Task-augmented tool calls now return CreateTaskResult with task as a direct field of result (result.task) instead of nested in _meta (result._meta.task), matching the MCP Tasks Specification (2025-11-25). Changes: - Modified handleToolCall() to return 'any' to support both CallToolResult and CreateTaskResult - Modified handleTaskAugmentedToolCall() to return *CreateTaskResult directly - Updated code generation templates to support ResultIsAny flag for flexible return types - Regenerated hooks.go and request_handler.go with updated templates - Fixed all tests to use pointer type assertions and expect correct format - Added comprehensive spec compliance tests All tests pass with race detector enabled. Resolves: #707 (TAS-1) * docs: update task spec URLs from draft to 2025-11-25 Updated doc comment spec version references for all task-related methods from specification/draft to specification/2025-11-25 for spec compliance. Changes: - MethodTasksGet - MethodTasksList - MethodTasksResult - MethodTasksCancel - MethodNotificationTasksStatus All 5 URLs now reference the published 2025-11-25 spec version. * docs: document input_required status and remove unused wrapper code - Add documentation to TaskStatusInputRequired explaining it's not yet implemented - Remove unused wrapper code in handleTaskAugmentedToolCall that was never executed - The wrapper was created but never used when hasTaskHandler=false Related to #707 * test: replace time.Sleep with proper synchronization in task tests Replace all time.Sleep calls in task_hooks_test.go with channels and sync.WaitGroup for reliable test synchronization. Update 7 test functions to use select/timeout pattern for single-event tests and WaitGroup for multi-event tests, eliminating race conditions and timing-dependent flakiness. Additionally: - Add model-immediate-response support with helper functions and tests - Improve task error differentiation (expired vs not found) - Remove unused wrapper code in handleTaskAugmentedToolCall - Add comprehensive test coverage for new functionality All tests pass with race detector enabled. * fix: improve task status notification test reliability and fix typo - Update task tool test to drain notifyChan until completed status is received, preventing flaky failures when working status arrives first - Fix grammar in README: 'timeout' -> 'time out' (verb form)
1 parent 45740a0 commit e2c1762

25 files changed

+5365
-59
lines changed

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,113 @@ The examples are simple but demonstrate the core concepts. Resources can be much
310310

311311
Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects. They're similar to POST endpoints in a REST API.
312312

313+
#### Task-Augmented Tools
314+
315+
Task-augmented tools execute asynchronously and return results via polling. This is useful for long-running operations that would otherwise block or time out. Task tools support three modes:
316+
317+
- **TaskSupportForbidden** (default): The tool cannot be invoked as a task
318+
- **TaskSupportOptional**: The tool can be invoked as a task or synchronously
319+
- **TaskSupportRequired**: The tool must be invoked as a task
320+
321+
```go
322+
// Example: A tool that requires task execution
323+
processBatchTool := mcp.NewTool("process_batch",
324+
mcp.WithDescription("Process a batch of items asynchronously"),
325+
mcp.WithTaskSupport(mcp.TaskSupportRequired),
326+
mcp.WithArray("items",
327+
mcp.Description("Array of items to process"),
328+
mcp.WithStringItems(),
329+
mcp.Required(),
330+
),
331+
)
332+
333+
// Task tool handler returns CreateTaskResult instead of CallToolResult
334+
s.AddTaskTool(processBatchTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CreateTaskResult, error) {
335+
items := request.GetStringSlice("items", []string{})
336+
337+
// Long-running work here
338+
for i, item := range items {
339+
select {
340+
case <-ctx.Done():
341+
// Task was cancelled
342+
return nil, ctx.Err()
343+
default:
344+
// Process item...
345+
processItem(item)
346+
}
347+
}
348+
349+
// Return result - task ID and metadata are managed by the server
350+
return &mcp.CreateTaskResult{
351+
Task: mcp.Task{
352+
// Task fields (ID, status, etc.) are populated by the server
353+
},
354+
}, nil
355+
})
356+
357+
// Enable task capabilities when creating the server
358+
s := server.NewMCPServer(
359+
"Task Server",
360+
"1.0.0",
361+
server.WithTaskCapabilities(
362+
true, // listTasks: allows clients to list all tasks
363+
true, // cancel: allows clients to cancel running tasks
364+
true, // toolCallTasks: enables task augmentation for tools
365+
),
366+
server.WithMaxConcurrentTasks(10), // Optional: limit concurrent running tasks
367+
)
368+
```
369+
370+
Task execution flow:
371+
1. Client calls tool with task parameter
372+
2. Server immediately returns task ID
373+
3. Tool executes asynchronously in the background
374+
4. Client polls `tasks/result` to retrieve the result
375+
5. Server sends task status notifications on completion
376+
377+
For optional task tools, the same tool can be called synchronously (without task parameter) or asynchronously (with task parameter):
378+
379+
```go
380+
// Tool with optional task support
381+
analyzeTool := mcp.NewTool("analyze_data",
382+
mcp.WithDescription("Analyze data - can run sync or async"),
383+
mcp.WithTaskSupport(mcp.TaskSupportOptional),
384+
mcp.WithString("data", mcp.Required()),
385+
)
386+
387+
// Use AddTaskTool for hybrid tools that support both modes
388+
s.AddTaskTool(analyzeTool, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CreateTaskResult, error) {
389+
// This handler runs when called as a task
390+
data := request.GetString("data", "")
391+
result := analyzeData(data)
392+
393+
return &mcp.CreateTaskResult{
394+
Task: mcp.Task{},
395+
}, nil
396+
})
397+
398+
// The server automatically handles both sync and async invocations
399+
// When called without task param: executes handler and returns immediately
400+
// When called with task param: executes handler asynchronously
401+
```
402+
403+
##### Limiting Concurrent Tasks
404+
405+
To prevent resource exhaustion, you can limit the number of concurrent running tasks:
406+
407+
```go
408+
s := server.NewMCPServer(
409+
"Task Server",
410+
"1.0.0",
411+
server.WithTaskCapabilities(true, true, true),
412+
server.WithMaxConcurrentTasks(10), // Allow up to 10 concurrent running tasks
413+
)
414+
```
415+
416+
When the limit is reached, new task creation requests will fail with an error. Completed, failed, or cancelled tasks don't count toward the limit - only tasks in "working" status. If `WithMaxConcurrentTasks` is not specified or set to 0, there is no limit on concurrent tasks.
417+
418+
For traditional synchronous tools that execute and return results immediately:
419+
313420
Simple calculation example:
314421
```go
315422
calculatorTool := mcp.NewTool("calculate",
@@ -535,6 +642,10 @@ Prompts can include:
535642

536643
For examples, see the [`examples/`](examples/) directory.
537644

645+
Key examples include:
646+
- [`examples/task_tool/`](examples/task_tool/) - Demonstrates task-augmented tools with TaskSupportRequired and TaskSupportOptional modes
647+
- Additional examples covering resources, prompts, and more in the examples directory
648+
538649
## Extras
539650

540651
### Transports

client/http_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515

1616
func TestHTTPClient(t *testing.T) {
1717
hooks := &server.Hooks{}
18-
hooks.AddAfterCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
18+
hooks.AddAfterCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest, result any) {
1919
clientSession := server.ClientSessionFromContext(ctx)
2020
// wait until all the notifications are handled
2121
for len(clientSession.NotificationChannel()) > 0 {

examples/everything/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func NewMCPServer() *server.MCPServer {
5454
hooks.AddAfterInitialize(func(ctx context.Context, id any, message *mcp.InitializeRequest, result *mcp.InitializeResult) {
5555
fmt.Printf("afterInitialize: %v, %v, %v\n", id, message, result)
5656
})
57-
hooks.AddAfterCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest, result *mcp.CallToolResult) {
57+
hooks.AddAfterCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest, result any) {
5858
fmt.Printf("afterCallTool: %v, %v, %v\n", id, message, result)
5959
})
6060
hooks.AddBeforeCallTool(func(ctx context.Context, id any, message *mcp.CallToolRequest) {

0 commit comments

Comments
 (0)