Files
cloudpods/vendor/github.com/mark3labs/mcp-go/server/server.go
2026-09-07 14:25:53 +08:00

2492 lines
70 KiB
Go

// Package server provides MCP (Model Context Protocol) server implementations.
package server
import (
"cmp"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"sort"
"sync"
"time"
"github.com/google/uuid"
"github.com/mark3labs/mcp-go/mcp"
)
// resourceEntry holds both a resource and its handler
type resourceEntry struct {
resource mcp.Resource
handler ResourceHandlerFunc
}
// resourceTemplateEntry holds both a template and its handler
type resourceTemplateEntry struct {
template mcp.ResourceTemplate
handler ResourceTemplateHandlerFunc
}
// taskEntry holds task state and associated data
type taskEntry struct {
task mcp.Task
sessionID string
toolName string // Name of the tool that created this task
createdAt time.Time // When the task was created (for metrics)
result any // The actual result once completed
resultErr error // Error if task failed
cancelFunc context.CancelFunc // Function to cancel the task
done chan struct{} // Channel to signal task completion
completed bool // Whether the task has been completed (guards done channel closure)
}
// ServerOption is a function that configures an MCPServer.
type ServerOption func(*MCPServer)
// ResourceHandlerFunc is a function that returns resource contents.
type ResourceHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
// ResourceTemplateHandlerFunc is a function that returns a resource template.
type ResourceTemplateHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
// PromptHandlerFunc handles prompt requests with given arguments.
type PromptHandlerFunc func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error)
// ToolHandlerFunc handles tool calls with given arguments.
type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
// TaskToolHandlerFunc handles tool calls that execute asynchronously.
// It returns immediately with task creation info; the actual result is
// retrieved later via tasks/result.
type TaskToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CreateTaskResult, error)
// ToolHandlerMiddleware is a middleware function that wraps a ToolHandlerFunc.
type ToolHandlerMiddleware func(ToolHandlerFunc) ToolHandlerFunc
// ResourceHandlerMiddleware is a middleware function that wraps a ResourceHandlerFunc.
type ResourceHandlerMiddleware func(ResourceHandlerFunc) ResourceHandlerFunc
// ToolFilterFunc is a function that filters tools based on context, typically using session information.
type ToolFilterFunc func(ctx context.Context, tools []mcp.Tool) []mcp.Tool
// PromptHandlerMiddleware is a middleware function that wraps a PromptHandlerFunc.
type PromptHandlerMiddleware func(PromptHandlerFunc) PromptHandlerFunc
// PromptFilterFunc is a function that filters prompts based on context, typically using session information.
type PromptFilterFunc func(ctx context.Context, prompts []mcp.Prompt) []mcp.Prompt
// ServerTool combines a Tool with its ToolHandlerFunc.
type ServerTool struct {
Tool mcp.Tool
Handler ToolHandlerFunc
}
// ServerTaskTool combines a Tool with its TaskToolHandlerFunc.
type ServerTaskTool struct {
Tool mcp.Tool
Handler TaskToolHandlerFunc
}
// ServerPrompt combines a Prompt with its handler function.
type ServerPrompt struct {
Prompt mcp.Prompt
Handler PromptHandlerFunc
}
// ServerResource combines a Resource with its handler function.
type ServerResource struct {
Resource mcp.Resource
Handler ResourceHandlerFunc
}
// ServerResourceTemplate combines a ResourceTemplate with its handler function.
type ServerResourceTemplate struct {
Template mcp.ResourceTemplate
Handler ResourceTemplateHandlerFunc
}
// serverKey is the context key for storing the server instance
type serverKey struct{}
// ServerFromContext retrieves the MCPServer instance from a context
func ServerFromContext(ctx context.Context) *MCPServer {
if srv, ok := ctx.Value(serverKey{}).(*MCPServer); ok {
return srv
}
return nil
}
// UnparsableMessageError is attached to the RequestError when json.Unmarshal
// fails on the request.
type UnparsableMessageError struct {
message json.RawMessage
method mcp.MCPMethod
err error
}
func (e *UnparsableMessageError) Error() string {
return fmt.Sprintf("unparsable %s request: %s", e.method, e.err)
}
func (e *UnparsableMessageError) Unwrap() error {
return e.err
}
func (e *UnparsableMessageError) GetMessage() json.RawMessage {
return e.message
}
func (e *UnparsableMessageError) GetMethod() mcp.MCPMethod {
return e.method
}
// RequestError is an error that can be converted to a JSON-RPC error.
// Implements Unwrap() to allow inspecting the error chain.
type requestError struct {
id any
code int
err error
}
func (e *requestError) Error() string {
return fmt.Sprintf("request error: %s", e.err)
}
func (e *requestError) ToJSONRPCError() mcp.JSONRPCError {
return mcp.JSONRPCError{
JSONRPC: mcp.JSONRPC_VERSION,
ID: mcp.NewRequestId(e.id),
Error: mcp.NewJSONRPCErrorDetails(e.code, e.err.Error(), nil),
}
}
func (e *requestError) Unwrap() error {
return e.err
}
// NotificationHandlerFunc handles incoming notifications.
type NotificationHandlerFunc func(ctx context.Context, notification mcp.JSONRPCNotification)
// MCPServer implements a Model Context Protocol server that can handle various types of requests
// including resources, prompts, and tools.
type MCPServer struct {
// Separate mutexes for different resource types
resourcesMu sync.RWMutex
resourceMiddlewareMu sync.RWMutex
promptsMu sync.RWMutex
toolsMu sync.RWMutex
toolMiddlewareMu sync.RWMutex
promptMiddlewareMu sync.RWMutex
notificationHandlersMu sync.RWMutex
capabilitiesMu sync.RWMutex
toolFiltersMu sync.RWMutex
promptFiltersMu sync.RWMutex
tasksMu sync.RWMutex
name string
version string
implementation mcp.Implementation
instructions string
resources map[string]resourceEntry
resourceTemplates map[string]resourceTemplateEntry
prompts map[string]mcp.Prompt
promptHandlers map[string]PromptHandlerFunc
tools map[string]ServerTool
taskTools map[string]ServerTaskTool
toolHandlerMiddlewares []ToolHandlerMiddleware
resourceHandlerMiddlewares []ResourceHandlerMiddleware
promptHandlerMiddlewares []PromptHandlerMiddleware
toolFilters []ToolFilterFunc
promptFilters []PromptFilterFunc
notificationHandlers map[string]NotificationHandlerFunc
promptCompletionProvider PromptCompletionProvider
resourceCompletionProvider ResourceCompletionProvider
capabilities serverCapabilities
paginationLimit *int
sessions sync.Map
hooks *Hooks
taskHooks *TaskHooks
tasks map[string]*taskEntry
expiredTasks map[string]time.Time // Tracks recently expired task IDs with expiration timestamp
maxConcurrentTasks *int // Optional limit on concurrent running tasks
activeTasks int // Current count of running (non-terminal) tasks
inflightCancels sync.Map // Maps request ID -> context.CancelFunc for in-flight requests
}
// WithPaginationLimit sets the pagination limit for the server.
func WithPaginationLimit(limit int) ServerOption {
return func(s *MCPServer) {
s.paginationLimit = &limit
}
}
// serverCapabilities defines the supported features of the MCP server
type serverCapabilities struct {
tools *toolCapabilities
resources *resourceCapabilities
prompts *promptCapabilities
logging *bool
sampling *bool
elicitation *bool
roots *bool
tasks *taskCapabilities
completions *bool
experimental map[string]any
}
// resourceCapabilities defines the supported resource-related features
type resourceCapabilities struct {
subscribe bool
listChanged bool
}
// promptCapabilities defines the supported prompt-related features
type promptCapabilities struct {
listChanged bool
}
// toolCapabilities defines the supported tool-related features
type toolCapabilities struct {
listChanged bool
}
// taskCapabilities defines the supported task-related features
type taskCapabilities struct {
list bool
cancel bool
toolCallTasks bool
}
// WithResourceCapabilities configures resource-related server capabilities
func WithResourceCapabilities(subscribe, listChanged bool) ServerOption {
return func(s *MCPServer) {
// Always create a non-nil capability object
s.capabilities.resources = &resourceCapabilities{
subscribe: subscribe,
listChanged: listChanged,
}
}
}
// WithPromptCompletionProvider sets a custom prompt completion provider
func WithPromptCompletionProvider(provider PromptCompletionProvider) ServerOption {
return func(s *MCPServer) {
s.promptCompletionProvider = provider
}
}
// WithResourceCompletionProvider sets a custom resource completion provider
func WithResourceCompletionProvider(provider ResourceCompletionProvider) ServerOption {
return func(s *MCPServer) {
s.resourceCompletionProvider = provider
}
}
// WithToolHandlerMiddleware allows adding a middleware for the
// tool handler call chain.
func WithToolHandlerMiddleware(
toolHandlerMiddleware ToolHandlerMiddleware,
) ServerOption {
return func(s *MCPServer) {
s.toolMiddlewareMu.Lock()
s.toolHandlerMiddlewares = append(s.toolHandlerMiddlewares, toolHandlerMiddleware)
s.toolMiddlewareMu.Unlock()
}
}
// Use adds one or more tool handler middlewares to the server.
// Middleware is applied in the order added (outermost first), matching net/http convention.
func (s *MCPServer) Use(mw ...ToolHandlerMiddleware) {
s.toolMiddlewareMu.Lock()
s.toolHandlerMiddlewares = append(s.toolHandlerMiddlewares, mw...)
s.toolMiddlewareMu.Unlock()
}
// WithResourceHandlerMiddleware allows adding a middleware for the
// resource handler call chain.
func WithResourceHandlerMiddleware(
resourceHandlerMiddleware ResourceHandlerMiddleware,
) ServerOption {
return func(s *MCPServer) {
s.resourceMiddlewareMu.Lock()
s.resourceHandlerMiddlewares = append(s.resourceHandlerMiddlewares, resourceHandlerMiddleware)
s.resourceMiddlewareMu.Unlock()
}
}
// WithResourceRecovery adds a middleware that recovers from panics in resource handlers.
func WithResourceRecovery() ServerOption {
return WithResourceHandlerMiddleware(func(next ResourceHandlerFunc) ResourceHandlerFunc {
return func(ctx context.Context, request mcp.ReadResourceRequest) (result []mcp.ResourceContents, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf(
"panic recovered in %s resource handler: %v",
request.Params.URI,
r,
)
}
}()
return next(ctx, request)
}
})
}
// WithToolFilter adds a filter function that will be applied to tools before they are returned in list_tools
func WithToolFilter(
toolFilter ToolFilterFunc,
) ServerOption {
return func(s *MCPServer) {
s.toolFiltersMu.Lock()
s.toolFilters = append(s.toolFilters, toolFilter)
s.toolFiltersMu.Unlock()
}
}
// WithPromptHandlerMiddleware allows adding a middleware for the
// prompt handler call chain.
func WithPromptHandlerMiddleware(
promptHandlerMiddleware PromptHandlerMiddleware,
) ServerOption {
return func(s *MCPServer) {
s.promptMiddlewareMu.Lock()
s.promptHandlerMiddlewares = append(s.promptHandlerMiddlewares, promptHandlerMiddleware)
s.promptMiddlewareMu.Unlock()
}
}
// WithPromptFilter adds a filter function that will be applied to prompts before they are returned in list_prompts
func WithPromptFilter(
promptFilter PromptFilterFunc,
) ServerOption {
return func(s *MCPServer) {
s.promptFiltersMu.Lock()
s.promptFilters = append(s.promptFilters, promptFilter)
s.promptFiltersMu.Unlock()
}
}
// WithRecovery adds a middleware that recovers from panics in tool handlers.
func WithRecovery() ServerOption {
return WithToolHandlerMiddleware(func(next ToolHandlerFunc) ToolHandlerFunc {
return func(ctx context.Context, request mcp.CallToolRequest) (result *mcp.CallToolResult, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf(
"panic recovered in %s tool handler: %v",
request.Params.Name,
r,
)
}
}()
return next(ctx, request)
}
})
}
// WithHooks allows adding hooks that will be called before or after
// either [all] requests or before / after specific request methods, or else
// prior to returning an error to the client.
func WithHooks(hooks *Hooks) ServerOption {
return func(s *MCPServer) {
s.hooks = hooks
}
}
// GetHooks returns the server's current Hooks instance, or nil if no hooks
// have been configured. The returned pointer can be used to add additional
// hooks via the Add* methods without replacing existing hook registrations.
func (s *MCPServer) GetHooks() *Hooks {
return s.hooks
}
// WithTaskHooks allows adding hooks for task lifecycle events.
// Use these hooks to monitor task execution, track metrics, and observe
// task-augmented tool behavior.
func WithTaskHooks(taskHooks *TaskHooks) ServerOption {
return func(s *MCPServer) {
s.taskHooks = taskHooks
}
}
// WithMaxConcurrentTasks sets a limit on the maximum number of concurrent running tasks.
// When this limit is reached, attempts to create new tasks will fail with an error.
// If not set (or set to 0), there is no limit on concurrent tasks.
func WithMaxConcurrentTasks(limit int) ServerOption {
return func(s *MCPServer) {
s.maxConcurrentTasks = &limit
}
}
// WithPromptCapabilities configures prompt-related server capabilities
func WithPromptCapabilities(listChanged bool) ServerOption {
return func(s *MCPServer) {
// Always create a non-nil capability object
s.capabilities.prompts = &promptCapabilities{
listChanged: listChanged,
}
}
}
// WithToolCapabilities configures tool-related server capabilities
func WithToolCapabilities(listChanged bool) ServerOption {
return func(s *MCPServer) {
// Always create a non-nil capability object
s.capabilities.tools = &toolCapabilities{
listChanged: listChanged,
}
}
}
// WithLogging enables logging capabilities for the server
func WithLogging() ServerOption {
return func(s *MCPServer) {
s.capabilities.logging = mcp.ToBoolPtr(true)
}
}
// WithElicitation enables elicitation capabilities for the server
func WithElicitation() ServerOption {
return func(s *MCPServer) {
s.capabilities.elicitation = mcp.ToBoolPtr(true)
}
}
// WithRoots returns a ServerOption that enables the roots capability on the MCPServer
func WithRoots() ServerOption {
return func(s *MCPServer) {
s.capabilities.roots = mcp.ToBoolPtr(true)
}
}
// WithTaskCapabilities configures task-related server capabilities
func WithTaskCapabilities(list, cancel, toolCallTasks bool) ServerOption {
return func(s *MCPServer) {
// Always create a non-nil capability object
s.capabilities.tasks = &taskCapabilities{
list: list,
cancel: cancel,
toolCallTasks: toolCallTasks,
}
}
}
// WithInstructions sets the server instructions for the client returned in the initialize response
func WithInstructions(instructions string) ServerOption {
return func(s *MCPServer) {
s.instructions = instructions
}
}
// WithCompletions enables the completion capability
func WithCompletions() ServerOption {
return func(s *MCPServer) {
s.capabilities.completions = mcp.ToBoolPtr(true)
}
}
// WithExperimental sets experimental, non-standard capabilities on the server.
func WithExperimental(experimental map[string]any) ServerOption {
return func(s *MCPServer) {
s.capabilities.experimental = experimental
}
}
// WithIcons sets the server icons for the implementation metadata returned
// during initialization. The icons slice and nested Sizes fields are defensively
// copied to prevent external mutation.
func WithIcons(icons ...mcp.Icon) ServerOption {
return func(s *MCPServer) {
copied := make([]mcp.Icon, len(icons))
for i, icon := range icons {
copied[i] = icon
if icon.Sizes != nil {
copied[i].Sizes = make([]string, len(icon.Sizes))
copy(copied[i].Sizes, icon.Sizes)
}
}
s.implementation.Icons = copied
}
}
// WithTitle sets the human-readable display title for the server implementation.
func WithTitle(title string) ServerOption {
return func(s *MCPServer) {
s.implementation.Title = title
}
}
// WithDescription sets the description for the server implementation.
func WithDescription(description string) ServerOption {
return func(s *MCPServer) {
s.implementation.Description = description
}
}
// WithWebsiteURL sets the website URL for the server implementation.
func WithWebsiteURL(websiteURL string) ServerOption {
return func(s *MCPServer) {
s.implementation.WebsiteURL = websiteURL
}
}
// NewMCPServer creates a new MCP server instance with the given name, version and options
func NewMCPServer(
name, version string,
opts ...ServerOption,
) *MCPServer {
s := &MCPServer{
resources: make(map[string]resourceEntry),
resourceTemplates: make(map[string]resourceTemplateEntry),
prompts: make(map[string]mcp.Prompt),
promptHandlers: make(map[string]PromptHandlerFunc),
tools: make(map[string]ServerTool),
taskTools: make(map[string]ServerTaskTool),
toolHandlerMiddlewares: make([]ToolHandlerMiddleware, 0),
resourceHandlerMiddlewares: make([]ResourceHandlerMiddleware, 0),
promptHandlerMiddlewares: make([]PromptHandlerMiddleware, 0),
name: name,
version: version,
notificationHandlers: make(map[string]NotificationHandlerFunc),
tasks: make(map[string]*taskEntry),
expiredTasks: make(map[string]time.Time),
promptCompletionProvider: &DefaultPromptCompletionProvider{},
resourceCompletionProvider: &DefaultResourceCompletionProvider{},
capabilities: serverCapabilities{
tools: nil,
resources: nil,
prompts: nil,
logging: nil,
sampling: nil,
elicitation: nil,
roots: nil,
tasks: nil,
completions: nil,
},
}
for _, opt := range opts {
opt(s)
}
return s
}
// GenerateInProcessSessionID generates a unique session ID for inprocess clients
func (s *MCPServer) GenerateInProcessSessionID() string {
return GenerateInProcessSessionID()
}
// AddResources registers multiple resources at once
func (s *MCPServer) AddResources(resources ...ServerResource) {
s.implicitlyRegisterResourceCapabilities()
s.resourcesMu.Lock()
for _, entry := range resources {
s.resources[entry.Resource.URI] = resourceEntry{
resource: entry.Resource,
handler: entry.Handler,
}
}
s.resourcesMu.Unlock()
// When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification
if s.capabilities.resources.listChanged {
// Send notification to all initialized sessions
s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil)
}
}
// SetResources replaces all existing resources with the provided list
func (s *MCPServer) SetResources(resources ...ServerResource) {
s.resourcesMu.Lock()
s.resources = make(map[string]resourceEntry, len(resources))
s.resourcesMu.Unlock()
s.AddResources(resources...)
}
// AddResource registers a new resource and its handler
func (s *MCPServer) AddResource(
resource mcp.Resource,
handler ResourceHandlerFunc,
) {
s.AddResources(ServerResource{Resource: resource, Handler: handler})
}
// DeleteResources removes resources from the server
func (s *MCPServer) DeleteResources(uris ...string) {
s.resourcesMu.Lock()
var exists bool
for _, uri := range uris {
if _, ok := s.resources[uri]; ok {
delete(s.resources, uri)
exists = true
}
}
s.resourcesMu.Unlock()
// Send notification to all initialized sessions if listChanged capability is enabled and we actually remove a resource
if exists && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil)
}
}
// RemoveResource removes a resource from the server
func (s *MCPServer) RemoveResource(uri string) {
s.resourcesMu.Lock()
_, exists := s.resources[uri]
if exists {
delete(s.resources, uri)
}
s.resourcesMu.Unlock()
// Send notification to all initialized sessions if listChanged capability is enabled and we actually remove a resource
if exists && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil)
}
}
// AddResourceTemplates registers multiple resource templates at once
func (s *MCPServer) AddResourceTemplates(resourceTemplates ...ServerResourceTemplate) {
s.implicitlyRegisterResourceCapabilities()
s.resourcesMu.Lock()
for _, entry := range resourceTemplates {
s.resourceTemplates[entry.Template.URITemplate.Raw()] = resourceTemplateEntry{
template: entry.Template,
handler: entry.Handler,
}
}
s.resourcesMu.Unlock()
// When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification
if s.capabilities.resources.listChanged {
// Send notification to all initialized sessions
s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil)
}
}
// SetResourceTemplates replaces all existing resource templates with the provided list
func (s *MCPServer) SetResourceTemplates(templates ...ServerResourceTemplate) {
s.resourcesMu.Lock()
s.resourceTemplates = make(map[string]resourceTemplateEntry, len(templates))
s.resourcesMu.Unlock()
s.AddResourceTemplates(templates...)
}
// AddResourceTemplate registers a new resource template and its handler
func (s *MCPServer) AddResourceTemplate(
template mcp.ResourceTemplate,
handler ResourceTemplateHandlerFunc,
) {
s.AddResourceTemplates(ServerResourceTemplate{Template: template, Handler: handler})
}
// AddPrompts registers multiple prompts at once
func (s *MCPServer) AddPrompts(prompts ...ServerPrompt) {
s.implicitlyRegisterPromptCapabilities()
s.promptsMu.Lock()
for _, entry := range prompts {
s.prompts[entry.Prompt.Name] = entry.Prompt
s.promptHandlers[entry.Prompt.Name] = entry.Handler
}
s.promptsMu.Unlock()
// When the list of available prompts changes, servers that declared the listChanged capability SHOULD send a notification.
if s.capabilities.prompts.listChanged {
// Send notification to all initialized sessions
s.SendNotificationToAllClients(mcp.MethodNotificationPromptsListChanged, nil)
}
}
// AddPrompt registers a new prompt handler with the given name
func (s *MCPServer) AddPrompt(prompt mcp.Prompt, handler PromptHandlerFunc) {
s.AddPrompts(ServerPrompt{Prompt: prompt, Handler: handler})
}
// SetPrompts replaces all existing prompts with the provided list
func (s *MCPServer) SetPrompts(prompts ...ServerPrompt) {
s.promptsMu.Lock()
s.prompts = make(map[string]mcp.Prompt, len(prompts))
s.promptHandlers = make(map[string]PromptHandlerFunc, len(prompts))
s.promptsMu.Unlock()
s.AddPrompts(prompts...)
}
// DeletePrompts removes prompts from the server
func (s *MCPServer) DeletePrompts(names ...string) {
s.promptsMu.Lock()
var exists bool
for _, name := range names {
if _, ok := s.prompts[name]; ok {
delete(s.prompts, name)
delete(s.promptHandlers, name)
exists = true
}
}
s.promptsMu.Unlock()
// Send notification to all initialized sessions if listChanged capability is enabled, and we actually remove a prompt
if exists && s.capabilities.prompts != nil && s.capabilities.prompts.listChanged {
// Send notification to all initialized sessions
s.SendNotificationToAllClients(mcp.MethodNotificationPromptsListChanged, nil)
}
}
// AddTool registers a new tool and its handler
func (s *MCPServer) AddTool(tool mcp.Tool, handler ToolHandlerFunc) {
s.AddTools(ServerTool{Tool: tool, Handler: handler})
}
// AddTaskTool registers a new task tool and its handler
func (s *MCPServer) AddTaskTool(tool mcp.Tool, handler TaskToolHandlerFunc) {
s.AddTaskTools(ServerTaskTool{Tool: tool, Handler: handler})
}
// Register tool capabilities due to a tool being added. Default to
// listChanged: true, but don't change the value if we've already explicitly
// registered tools.listChanged false.
func (s *MCPServer) implicitlyRegisterToolCapabilities() {
s.implicitlyRegisterCapabilities(
func() bool { return s.capabilities.tools != nil },
func() { s.capabilities.tools = &toolCapabilities{listChanged: true} },
)
}
func (s *MCPServer) implicitlyRegisterResourceCapabilities() {
s.implicitlyRegisterCapabilities(
func() bool { return s.capabilities.resources != nil },
func() { s.capabilities.resources = &resourceCapabilities{} },
)
}
func (s *MCPServer) implicitlyRegisterPromptCapabilities() {
s.implicitlyRegisterCapabilities(
func() bool { return s.capabilities.prompts != nil },
func() { s.capabilities.prompts = &promptCapabilities{} },
)
}
func (s *MCPServer) implicitlyRegisterCapabilities(check func() bool, register func()) {
s.capabilitiesMu.RLock()
if check() {
s.capabilitiesMu.RUnlock()
return
}
s.capabilitiesMu.RUnlock()
s.capabilitiesMu.Lock()
if !check() {
register()
}
s.capabilitiesMu.Unlock()
}
// AddTools registers multiple tools at once
func (s *MCPServer) AddTools(tools ...ServerTool) {
s.implicitlyRegisterToolCapabilities()
s.toolsMu.Lock()
for _, entry := range tools {
name := entry.Tool.Name
// Check for collision with task tools
if _, exists := s.taskTools[name]; exists {
s.toolsMu.Unlock()
panic(fmt.Sprintf("tool name '%s' already registered as task tool", name))
}
s.tools[name] = entry
}
s.toolsMu.Unlock()
// When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification.
if s.capabilities.tools.listChanged {
// Send notification to all initialized sessions
s.SendNotificationToAllClients(mcp.MethodNotificationToolsListChanged, nil)
}
}
// AddTaskTools registers multiple task tools at once
func (s *MCPServer) AddTaskTools(taskTools ...ServerTaskTool) {
s.implicitlyRegisterToolCapabilities()
s.toolsMu.Lock()
for _, entry := range taskTools {
name := entry.Tool.Name
// Check for collision with regular tools
if _, exists := s.tools[name]; exists {
s.toolsMu.Unlock()
panic(fmt.Sprintf("task tool name '%s' already registered as regular tool", name))
}
s.taskTools[name] = entry
}
s.toolsMu.Unlock()
// When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification.
if s.capabilities.tools.listChanged {
// Send notification to all initialized sessions
s.SendNotificationToAllClients(mcp.MethodNotificationToolsListChanged, nil)
}
}
// SetTools replaces all existing tools with the provided list
func (s *MCPServer) SetTools(tools ...ServerTool) {
s.toolsMu.Lock()
s.tools = make(map[string]ServerTool, len(tools))
s.toolsMu.Unlock()
s.AddTools(tools...)
}
// GetTool retrieves the specified tool
func (s *MCPServer) GetTool(toolName string) *ServerTool {
s.toolsMu.RLock()
defer s.toolsMu.RUnlock()
if tool, ok := s.tools[toolName]; ok {
return &tool
}
return nil
}
func (s *MCPServer) ListTools() map[string]*ServerTool {
s.toolsMu.RLock()
defer s.toolsMu.RUnlock()
if len(s.tools) == 0 {
return nil
}
// Create a copy to prevent external modification
toolsCopy := make(map[string]*ServerTool, len(s.tools))
for name, tool := range s.tools {
toolsCopy[name] = &tool
}
return toolsCopy
}
// DeleteTools removes tools from the server
func (s *MCPServer) DeleteTools(names ...string) {
s.toolsMu.Lock()
var exists bool
for _, name := range names {
if _, ok := s.tools[name]; ok {
delete(s.tools, name)
exists = true
}
}
s.toolsMu.Unlock()
// When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification.
if exists && s.capabilities.tools != nil && s.capabilities.tools.listChanged {
// Send notification to all initialized sessions
s.SendNotificationToAllClients(mcp.MethodNotificationToolsListChanged, nil)
}
}
// AddNotificationHandler registers a new handler for incoming notifications
func (s *MCPServer) AddNotificationHandler(
method string,
handler NotificationHandlerFunc,
) {
s.notificationHandlersMu.Lock()
defer s.notificationHandlersMu.Unlock()
s.notificationHandlers[method] = handler
}
func (s *MCPServer) handleInitialize(
ctx context.Context,
_ any,
request mcp.InitializeRequest,
) (*mcp.InitializeResult, *requestError) {
capabilities := mcp.ServerCapabilities{}
// Only add resource capabilities if they're configured
if s.capabilities.resources != nil {
capabilities.Resources = &struct {
Subscribe bool `json:"subscribe,omitempty"`
ListChanged bool `json:"listChanged,omitempty"`
}{
Subscribe: s.capabilities.resources.subscribe,
ListChanged: s.capabilities.resources.listChanged,
}
}
// Only add prompt capabilities if they're configured
if s.capabilities.prompts != nil {
capabilities.Prompts = &struct {
ListChanged bool `json:"listChanged,omitempty"`
}{
ListChanged: s.capabilities.prompts.listChanged,
}
}
// Only add tool capabilities if they're configured
if s.capabilities.tools != nil {
capabilities.Tools = &struct {
ListChanged bool `json:"listChanged,omitempty"`
}{
ListChanged: s.capabilities.tools.listChanged,
}
}
if s.capabilities.logging != nil && *s.capabilities.logging {
capabilities.Logging = &struct{}{}
}
if s.capabilities.sampling != nil && *s.capabilities.sampling {
capabilities.Sampling = &struct{}{}
}
if s.capabilities.elicitation != nil && *s.capabilities.elicitation {
capabilities.Elicitation = &mcp.ElicitationCapability{}
}
if s.capabilities.roots != nil && *s.capabilities.roots {
capabilities.Roots = &struct{}{}
}
// Only add task capabilities if they're configured
if s.capabilities.tasks != nil {
tasksCapability := &mcp.TasksCapability{}
if s.capabilities.tasks.list {
tasksCapability.List = &struct{}{}
}
if s.capabilities.tasks.cancel {
tasksCapability.Cancel = &struct{}{}
}
if s.capabilities.tasks.toolCallTasks {
tasksCapability.Requests = &mcp.TaskRequestsCapability{
Tools: &struct {
Call *struct{} `json:"call,omitempty"`
}{
Call: &struct{}{},
},
}
}
capabilities.Tasks = tasksCapability
}
if s.capabilities.completions != nil && *s.capabilities.completions {
capabilities.Completions = &struct{}{}
}
if s.capabilities.experimental != nil {
capabilities.Experimental = s.capabilities.experimental
}
result := mcp.InitializeResult{
ProtocolVersion: s.protocolVersion(request.Params.ProtocolVersion),
ServerInfo: mcp.Implementation{
Name: s.name,
Version: s.version,
Title: s.implementation.Title,
Description: s.implementation.Description,
WebsiteURL: s.implementation.WebsiteURL,
Icons: s.implementation.Icons,
},
Capabilities: capabilities,
Instructions: s.instructions,
}
if session := ClientSessionFromContext(ctx); session != nil {
session.Initialize()
// Store client info if the session supports it
if sessionWithClientInfo, ok := session.(SessionWithClientInfo); ok {
sessionWithClientInfo.SetClientInfo(request.Params.ClientInfo)
sessionWithClientInfo.SetClientCapabilities(request.Params.Capabilities)
}
}
return &result, nil
}
func (s *MCPServer) protocolVersion(clientVersion string) string {
// For backwards compatibility, if the server does not receive an MCP-Protocol-Version header,
// and has no other way to identify the version - for example, by relying on the protocol version negotiated
// during initialization - the server SHOULD assume protocol version 2025-03-26
// https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header
if len(clientVersion) == 0 {
clientVersion = "2025-03-26"
}
if slices.Contains(mcp.ValidProtocolVersions, clientVersion) {
return clientVersion
}
return mcp.LATEST_PROTOCOL_VERSION
}
func (s *MCPServer) handlePing(
_ context.Context,
_ any,
_ mcp.PingRequest,
) (*mcp.EmptyResult, *requestError) {
return &mcp.EmptyResult{}, nil
}
func (s *MCPServer) handleSetLevel(
ctx context.Context,
id any,
request mcp.SetLevelRequest,
) (*mcp.EmptyResult, *requestError) {
clientSession := ClientSessionFromContext(ctx)
if clientSession == nil || !clientSession.Initialized() {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: ErrSessionNotInitialized,
}
}
sessionLogging, ok := clientSession.(SessionWithLogging)
if !ok {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: ErrSessionDoesNotSupportLogging,
}
}
level := request.Params.Level
// Validate logging level
switch level {
case mcp.LoggingLevelDebug, mcp.LoggingLevelInfo, mcp.LoggingLevelNotice,
mcp.LoggingLevelWarning, mcp.LoggingLevelError, mcp.LoggingLevelCritical,
mcp.LoggingLevelAlert, mcp.LoggingLevelEmergency:
// Valid level
default:
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: fmt.Errorf("invalid logging level '%s'", level),
}
}
sessionLogging.SetLogLevel(level)
return &mcp.EmptyResult{}, nil
}
func listByPagination[T mcp.Named](
_ context.Context,
s *MCPServer,
cursor mcp.Cursor,
allElements []T,
) ([]T, mcp.Cursor, error) {
startPos := 0
if cursor != "" {
c, err := base64.StdEncoding.DecodeString(string(cursor))
if err != nil {
return nil, "", err
}
cString := string(c)
startPos = sort.Search(len(allElements), func(i int) bool {
return allElements[i].GetName() > cString
})
}
endPos := len(allElements)
if s.paginationLimit != nil {
if len(allElements) > startPos+*s.paginationLimit {
endPos = startPos + *s.paginationLimit
}
}
elementsToReturn := allElements[startPos:endPos]
// set the next cursor
nextCursor := func() mcp.Cursor {
if s.paginationLimit != nil && len(elementsToReturn) >= *s.paginationLimit {
nc := elementsToReturn[len(elementsToReturn)-1].GetName()
toString := base64.StdEncoding.EncodeToString([]byte(nc))
return mcp.Cursor(toString)
}
return ""
}()
return elementsToReturn, nextCursor, nil
}
func (s *MCPServer) handleListResources(
ctx context.Context,
id any,
request mcp.ListResourcesRequest,
) (*mcp.ListResourcesResult, *requestError) {
s.resourcesMu.RLock()
resourceMap := make(map[string]mcp.Resource, len(s.resources))
for uri, entry := range s.resources {
resourceMap[uri] = entry.resource
}
s.resourcesMu.RUnlock()
// Check if there are session-specific resources
session := ClientSessionFromContext(ctx)
if session != nil {
if sessionWithResources, ok := session.(SessionWithResources); ok {
if sessionResources := sessionWithResources.GetSessionResources(); sessionResources != nil {
// Merge session-specific resources with global resources
for uri, serverResource := range sessionResources {
resourceMap[uri] = serverResource.Resource
}
}
}
}
// Sort the resources by name
resourcesList := slices.SortedFunc(maps.Values(resourceMap), func(a, b mcp.Resource) int {
return cmp.Compare(a.Name, b.Name)
})
// Apply pagination
resourcesToReturn, nextCursor, err := listByPagination(
ctx,
s,
request.Params.Cursor,
resourcesList,
)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
if resourcesToReturn == nil {
resourcesToReturn = []mcp.Resource{}
}
result := mcp.ListResourcesResult{
Resources: resourcesToReturn,
PaginatedResult: mcp.PaginatedResult{
NextCursor: nextCursor,
},
}
return &result, nil
}
func (s *MCPServer) handleListResourceTemplates(
ctx context.Context,
id any,
request mcp.ListResourceTemplatesRequest,
) (*mcp.ListResourceTemplatesResult, *requestError) {
// Get global templates
s.resourcesMu.RLock()
templateMap := make(map[string]mcp.ResourceTemplate, len(s.resourceTemplates))
for uri, entry := range s.resourceTemplates {
templateMap[uri] = entry.template
}
s.resourcesMu.RUnlock()
// Check if there are session-specific resource templates
session := ClientSessionFromContext(ctx)
if session != nil {
if sessionWithTemplates, ok := session.(SessionWithResourceTemplates); ok {
if sessionTemplates := sessionWithTemplates.GetSessionResourceTemplates(); sessionTemplates != nil {
// Merge session-specific templates with global templates
// Session templates override global ones
for uriTemplate, serverTemplate := range sessionTemplates {
templateMap[uriTemplate] = serverTemplate.Template
}
}
}
}
// Convert map to slice for sorting and pagination
templates := make([]mcp.ResourceTemplate, 0, len(templateMap))
for _, template := range templateMap {
templates = append(templates, template)
}
sort.Slice(templates, func(i, j int) bool {
return templates[i].Name < templates[j].Name
})
templatesToReturn, nextCursor, err := listByPagination(
ctx,
s,
request.Params.Cursor,
templates,
)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
result := mcp.ListResourceTemplatesResult{
ResourceTemplates: templatesToReturn,
PaginatedResult: mcp.PaginatedResult{
NextCursor: nextCursor,
},
}
return &result, nil
}
func (s *MCPServer) handleReadResource(
ctx context.Context,
id any,
request mcp.ReadResourceRequest,
) (*mcp.ReadResourceResult, *requestError) {
s.resourcesMu.RLock()
// First check session-specific resources
var handler ResourceHandlerFunc
var ok bool
session := ClientSessionFromContext(ctx)
if session != nil {
if sessionWithResources, typeAssertOk := session.(SessionWithResources); typeAssertOk {
if sessionResources := sessionWithResources.GetSessionResources(); sessionResources != nil {
resource, sessionOk := sessionResources[request.Params.URI]
if sessionOk {
handler = resource.Handler
ok = true
}
}
}
}
// If not found in session tools, check global tools
if !ok {
globalResource, rok := s.resources[request.Params.URI]
if rok {
handler = globalResource.handler
ok = true
}
}
// First try direct resource handlers
if ok {
s.resourcesMu.RUnlock()
finalHandler := handler
s.resourceMiddlewareMu.RLock()
mw := s.resourceHandlerMiddlewares
// Apply middlewares in reverse order
for i := len(mw) - 1; i >= 0; i-- {
finalHandler = mw[i](finalHandler)
}
s.resourceMiddlewareMu.RUnlock()
contents, err := finalHandler(ctx, request)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: err,
}
}
return &mcp.ReadResourceResult{Contents: contents}, nil
}
// If no direct handler found, try matching against templates
var matchedHandler ResourceTemplateHandlerFunc
var matched bool
// First check session templates if available
if session != nil {
if sessionWithTemplates, ok := session.(SessionWithResourceTemplates); ok {
sessionTemplates := sessionWithTemplates.GetSessionResourceTemplates()
for _, serverTemplate := range sessionTemplates {
if serverTemplate.Template.URITemplate == nil {
continue
}
if matchesTemplate(request.Params.URI, serverTemplate.Template.URITemplate) {
matchedHandler = serverTemplate.Handler
matched = true
matchedVars := serverTemplate.Template.URITemplate.Match(request.Params.URI)
// Convert matched variables to a map
request.Params.Arguments = make(map[string]any, len(matchedVars))
for name, value := range matchedVars {
request.Params.Arguments[name] = value.V
}
break
}
}
}
}
// If not found in session templates, check global templates
if !matched {
for _, entry := range s.resourceTemplates {
template := entry.template
if template.URITemplate == nil {
continue
}
if matchesTemplate(request.Params.URI, template.URITemplate) {
matchedHandler = entry.handler
matched = true
matchedVars := template.URITemplate.Match(request.Params.URI)
// Convert matched variables to a map
request.Params.Arguments = make(map[string]any, len(matchedVars))
for name, value := range matchedVars {
request.Params.Arguments[name] = value.V
}
break
}
}
}
s.resourcesMu.RUnlock()
if matched {
// If a match is found, then we have a final handler and can
// apply middlewares.
s.resourceMiddlewareMu.RLock()
finalHandler := ResourceHandlerFunc(matchedHandler)
mw := s.resourceHandlerMiddlewares
// Apply middlewares in reverse order
for i := len(mw) - 1; i >= 0; i-- {
finalHandler = mw[i](finalHandler)
}
s.resourceMiddlewareMu.RUnlock()
contents, err := finalHandler(ctx, request)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: err,
}
}
return &mcp.ReadResourceResult{Contents: contents}, nil
}
return nil, &requestError{
id: id,
code: mcp.RESOURCE_NOT_FOUND,
err: fmt.Errorf(
"handler not found for resource URI '%s': %w",
request.Params.URI,
ErrResourceNotFound,
),
}
}
// matchesTemplate checks if a URI matches a URI template pattern
func matchesTemplate(uri string, template *mcp.URITemplate) bool {
return template.Regexp().MatchString(uri)
}
func (s *MCPServer) handleListPrompts(
ctx context.Context,
id any,
request mcp.ListPromptsRequest,
) (*mcp.ListPromptsResult, *requestError) {
s.promptsMu.RLock()
prompts := make([]mcp.Prompt, 0, len(s.prompts))
for _, prompt := range s.prompts {
prompts = append(prompts, prompt)
}
s.promptsMu.RUnlock()
// sort prompts by name
sort.Slice(prompts, func(i, j int) bool {
return prompts[i].Name < prompts[j].Name
})
// Apply prompt filters if any are defined
s.promptFiltersMu.RLock()
if len(s.promptFilters) > 0 {
for _, filter := range s.promptFilters {
prompts = filter(ctx, prompts)
}
}
s.promptFiltersMu.RUnlock()
promptsToReturn, nextCursor, err := listByPagination(
ctx,
s,
request.Params.Cursor,
prompts,
)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
result := mcp.ListPromptsResult{
Prompts: promptsToReturn,
PaginatedResult: mcp.PaginatedResult{
NextCursor: nextCursor,
},
}
return &result, nil
}
func (s *MCPServer) handleGetPrompt(
ctx context.Context,
id any,
request mcp.GetPromptRequest,
) (*mcp.GetPromptResult, *requestError) {
s.promptsMu.RLock()
handler, ok := s.promptHandlers[request.Params.Name]
s.promptsMu.RUnlock()
if !ok {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: fmt.Errorf("prompt '%s' not found: %w", request.Params.Name, ErrPromptNotFound),
}
}
finalHandler := handler
s.promptMiddlewareMu.RLock()
mw := s.promptHandlerMiddlewares
// Apply middlewares in reverse order
for i := len(mw) - 1; i >= 0; i-- {
finalHandler = mw[i](finalHandler)
}
s.promptMiddlewareMu.RUnlock()
result, err := finalHandler(ctx, request)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: err,
}
}
return result, nil
}
func (s *MCPServer) handleListTools(
ctx context.Context,
id any,
request mcp.ListToolsRequest,
) (*mcp.ListToolsResult, *requestError) {
// Get the base tools from the server (both regular and task tools)
s.toolsMu.RLock()
tools := make([]mcp.Tool, 0, len(s.tools)+len(s.taskTools))
// Get all tool names for consistent ordering
toolNames := make([]string, 0, len(s.tools)+len(s.taskTools))
for name := range s.tools {
toolNames = append(toolNames, name)
}
for name := range s.taskTools {
toolNames = append(toolNames, name)
}
// Sort the tool names for consistent ordering
sort.Strings(toolNames)
// Add tools in sorted order
for _, name := range toolNames {
if tool, ok := s.tools[name]; ok {
tools = append(tools, tool.Tool)
} else if taskTool, ok := s.taskTools[name]; ok {
tools = append(tools, taskTool.Tool)
}
}
s.toolsMu.RUnlock()
// Check if there are session-specific tools
session := ClientSessionFromContext(ctx)
if session != nil {
if sessionWithTools, ok := session.(SessionWithTools); ok {
if sessionTools := sessionWithTools.GetSessionTools(); sessionTools != nil {
// Override or add session-specific tools
// We need to create a map first to merge the tools properly
toolMap := make(map[string]mcp.Tool)
// Add global tools first
for _, tool := range tools {
toolMap[tool.Name] = tool
}
// Then override with session-specific tools
for name, serverTool := range sessionTools {
toolMap[name] = serverTool.Tool
}
// Convert back to slice
tools = make([]mcp.Tool, 0, len(toolMap))
for _, tool := range toolMap {
tools = append(tools, tool)
}
// Sort again to maintain consistent ordering
sort.Slice(tools, func(i, j int) bool {
return tools[i].Name < tools[j].Name
})
}
}
}
// Apply tool filters if any are defined
s.toolFiltersMu.RLock()
if len(s.toolFilters) > 0 {
for _, filter := range s.toolFilters {
tools = filter(ctx, tools)
}
}
s.toolFiltersMu.RUnlock()
// Apply pagination
toolsToReturn, nextCursor, err := listByPagination(
ctx,
s,
request.Params.Cursor,
tools,
)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
result := mcp.ListToolsResult{
Tools: toolsToReturn,
PaginatedResult: mcp.PaginatedResult{
NextCursor: nextCursor,
},
}
return &result, nil
}
func (s *MCPServer) handleToolCall(
ctx context.Context,
id any,
request mcp.CallToolRequest,
) (any, *requestError) {
// First check session-specific tools
var tool ServerTool
var ok bool
var taskToolOnly bool
session := ClientSessionFromContext(ctx)
if session != nil {
if sessionWithTools, typeAssertOk := session.(SessionWithTools); typeAssertOk {
if sessionTools := sessionWithTools.GetSessionTools(); sessionTools != nil {
var sessionOk bool
tool, sessionOk = sessionTools[request.Params.Name]
if sessionOk {
ok = true
}
}
}
}
// If not found in session tools, check global tools
if !ok {
s.toolsMu.RLock()
tool, ok = s.tools[request.Params.Name]
// If not in regular tools, check task tools
if !ok {
if taskTool, taskOk := s.taskTools[request.Params.Name]; taskOk {
// Convert ServerTaskTool to ServerTool for validation
// The tool metadata is the same, we just need it for checking task support
tool = ServerTool{
Tool: taskTool.Tool,
Handler: nil, // Handler will be used from taskTool in handleTaskAugmentedToolCall
}
ok = true
taskToolOnly = true
}
}
s.toolsMu.RUnlock()
}
if !ok {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: fmt.Errorf("tool '%s' not found: %w", request.Params.Name, ErrToolNotFound),
}
}
// Validate task support requirements
if tool.Tool.Execution != nil && tool.Tool.Execution.TaskSupport == mcp.TaskSupportRequired {
if request.Params.Task == nil {
return nil, &requestError{
id: id,
code: mcp.METHOD_NOT_FOUND,
err: fmt.Errorf("tool '%s' requires task augmentation", request.Params.Name),
}
}
}
// Check if this should be executed as a task (hybrid mode support)
// Tools with TaskSupportOptional or TaskSupportRequired can be executed as tasks
shouldExecuteAsTask := request.Params.Task != nil &&
tool.Tool.Execution != nil &&
(tool.Tool.Execution.TaskSupport == mcp.TaskSupportOptional ||
tool.Tool.Execution.TaskSupport == mcp.TaskSupportRequired)
if shouldExecuteAsTask {
// Route to task-augmented execution handler
return s.handleTaskAugmentedToolCall(ctx, id, request)
}
if taskToolOnly {
return nil, &requestError{
id: id,
code: mcp.METHOD_NOT_FOUND,
err: fmt.Errorf("tool '%s' does not support synchronous execution", request.Params.Name),
}
}
finalHandler := tool.Handler
s.toolMiddlewareMu.RLock()
mw := s.toolHandlerMiddlewares
// Apply middlewares in reverse order
for i := len(mw) - 1; i >= 0; i-- {
finalHandler = mw[i](finalHandler)
}
s.toolMiddlewareMu.RUnlock()
result, err := finalHandler(ctx, request)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: err,
}
}
return result, nil
}
// handleTaskAugmentedToolCall handles tool calls that are executed as tasks.
// It creates a task entry, starts async execution, and returns CreateTaskResult immediately.
func (s *MCPServer) handleTaskAugmentedToolCall(
ctx context.Context,
id any,
request mcp.CallToolRequest,
) (*mcp.CreateTaskResult, *requestError) {
// Look up the tool - check both taskTools and regular tools
s.toolsMu.RLock()
taskTool, isTaskTool := s.taskTools[request.Params.Name]
regularTool, isRegularTool := s.tools[request.Params.Name]
s.toolsMu.RUnlock()
// Determine which tool to use and validate task support
var toolToUse ServerTaskTool
var hasTaskHandler bool
if isTaskTool {
// Tool is registered as a task tool
toolToUse = taskTool
hasTaskHandler = true
} else if isRegularTool {
// Tool is a regular tool with task support
// Validate that it actually supports task augmentation
if regularTool.Tool.Execution == nil ||
(regularTool.Tool.Execution.TaskSupport != mcp.TaskSupportOptional &&
regularTool.Tool.Execution.TaskSupport != mcp.TaskSupportRequired) {
return nil, &requestError{
id: id,
code: mcp.METHOD_NOT_FOUND,
err: fmt.Errorf("tool '%s' does not support task augmentation", request.Params.Name),
}
}
hasTaskHandler = false
} else {
// Tool not found in either map
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: fmt.Errorf("tool '%s' not found", request.Params.Name),
}
}
// Generate task ID (UUID v4)
taskID := uuid.New().String()
// Extract TTL from task params
var ttl *int64
if request.Params.Task != nil {
ttl = request.Params.Task.TTL
}
// Create task entry (pollInterval is nil - server doesn't set a default)
entry, err := s.createTask(ctx, taskID, request.Params.Name, ttl, nil)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: err,
}
}
// Execute tool asynchronously
// For regular tools being used as tasks, we need different execution logic
if hasTaskHandler {
go s.executeTaskTool(ctx, entry, toolToUse, request)
} else {
// Execute regular tool wrapped as a task
go s.executeRegularToolAsTask(ctx, entry, regularTool, request)
}
// Return CreateTaskResult immediately with task as top-level field
// Make a copy of the task to avoid data races with background goroutine
s.tasksMu.RLock()
taskCopy := entry.task
s.tasksMu.RUnlock()
return &mcp.CreateTaskResult{
Task: taskCopy,
}, nil
}
// executeTaskTool executes a task tool handler asynchronously.
// It creates a cancellable context, stores the cancel function for potential cancellation,
// and executes the handler in the background, storing the result when complete.
func (s *MCPServer) executeTaskTool(
ctx context.Context,
entry *taskEntry,
taskTool ServerTaskTool,
request mcp.CallToolRequest,
) {
// Create cancellable context for this task execution
taskCtx, cancel := context.WithCancel(ctx)
defer cancel()
// Store cancel func in entry so it can be cancelled via tasks/cancel
s.tasksMu.Lock()
entry.cancelFunc = cancel
s.tasksMu.Unlock()
// Execute the task tool handler
result, err := taskTool.Handler(taskCtx, request)
if err != nil {
// If the error is due to context cancellation, don't mark as failed.
// The cancelTask method will handle setting the proper status.
// However, if cancelTask hasn't been called yet, we should still mark it.
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
// Check if task was already cancelled via tasks/cancel
s.tasksMu.Lock()
alreadyCancelled := entry.task.Status == mcp.TaskStatusCancelled
s.tasksMu.Unlock()
if !alreadyCancelled {
// Handler detected cancellation before tasks/cancel was called
// Mark as cancelled with the context error message
cancelledAt := time.Now()
duration := cancelledAt.Sub(entry.createdAt)
s.tasksMu.Lock()
if !entry.completed {
entry.task.Status = mcp.TaskStatusCancelled
entry.task.StatusMessage = err.Error()
entry.task.LastUpdatedAt = cancelledAt.UTC().Format(time.RFC3339)
entry.completed = true
close(entry.done)
// Decrement active tasks counter
s.activeTasks--
s.sendTaskStatusNotification(entry.task)
// Fire task cancellation hook
if s.taskHooks != nil {
metrics := TaskMetrics{
TaskID: entry.task.TaskId,
ToolName: entry.toolName,
Status: entry.task.Status,
StatusMessage: entry.task.StatusMessage,
CreatedAt: entry.createdAt,
CompletedAt: &cancelledAt,
Duration: duration,
SessionID: entry.sessionID,
}
s.taskHooks.taskCancelled(ctx, metrics)
}
}
s.tasksMu.Unlock()
}
return
}
// Task failed - complete with error
s.completeTask(entry, nil, err)
return
}
// Task succeeded - store the CreateTaskResult
// Note: The actual result will be retrieved later via tasks/result
s.completeTask(entry, result, nil)
}
// executeRegularToolAsTask executes a regular tool handler asynchronously as a task.
// This is used for hybrid mode where a tool with TaskSupportOptional is called with task params.
func (s *MCPServer) executeRegularToolAsTask(
ctx context.Context,
entry *taskEntry,
regularTool ServerTool,
request mcp.CallToolRequest,
) {
// Create cancellable context for this task execution
taskCtx, cancel := context.WithCancel(ctx)
defer cancel()
// Store cancel func in entry so it can be cancelled via tasks/cancel
s.tasksMu.Lock()
entry.cancelFunc = cancel
s.tasksMu.Unlock()
// Execute the regular tool handler with middleware applied
finalHandler := regularTool.Handler
s.toolMiddlewareMu.RLock()
mw := s.toolHandlerMiddlewares
for i := len(mw) - 1; i >= 0; i-- {
finalHandler = mw[i](finalHandler)
}
s.toolMiddlewareMu.RUnlock()
result, err := finalHandler(taskCtx, request)
if err != nil {
// If the error is due to context cancellation, don't mark as failed.
// The cancelTask method will handle setting the proper status.
// However, if cancelTask hasn't been called yet, we should still mark it.
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
// Check if task was already cancelled via tasks/cancel
s.tasksMu.Lock()
alreadyCancelled := entry.task.Status == mcp.TaskStatusCancelled
s.tasksMu.Unlock()
if !alreadyCancelled {
// Handler detected cancellation before tasks/cancel was called
// Mark as cancelled with the context error message
cancelledAt := time.Now()
duration := cancelledAt.Sub(entry.createdAt)
s.tasksMu.Lock()
if !entry.completed {
entry.task.Status = mcp.TaskStatusCancelled
entry.task.StatusMessage = err.Error()
entry.task.LastUpdatedAt = cancelledAt.UTC().Format(time.RFC3339)
entry.completed = true
close(entry.done)
// Decrement active tasks counter
s.activeTasks--
s.sendTaskStatusNotification(entry.task)
// Fire task cancellation hook
if s.taskHooks != nil {
metrics := TaskMetrics{
TaskID: entry.task.TaskId,
ToolName: entry.toolName,
Status: entry.task.Status,
StatusMessage: entry.task.StatusMessage,
CreatedAt: entry.createdAt,
CompletedAt: &cancelledAt,
Duration: duration,
SessionID: entry.sessionID,
}
s.taskHooks.taskCancelled(ctx, metrics)
}
}
s.tasksMu.Unlock()
}
return
}
// Task failed - complete with error
s.completeTask(entry, nil, err)
return
}
// Task succeeded - store the CallToolResult directly
// When retrieved via tasks/result, this will be returned to the client
s.completeTask(entry, result, nil)
}
func (s *MCPServer) handleNotification(
ctx context.Context,
notification mcp.JSONRPCNotification,
) mcp.JSONRPCMessage {
// Handle cancellation notifications per MCP spec
if notification.Method == "notifications/cancelled" {
if reqID, ok := notification.Params.AdditionalFields["requestId"]; ok {
key := inflightKey(ctx, reqID)
if cancel, loaded := s.inflightCancels.LoadAndDelete(key); loaded {
if cancelFunc, ok := cancel.(context.CancelFunc); ok {
cancelFunc()
}
}
}
return nil
}
s.notificationHandlersMu.RLock()
handler, ok := s.notificationHandlers[notification.Method]
s.notificationHandlersMu.RUnlock()
if ok {
handler(ctx, notification)
}
return nil
}
// inflightKey returns a session-scoped key for the inflight cancellation map.
// This prevents cross-session request ID collisions in multi-client scenarios.
func inflightKey(ctx context.Context, requestID any) string {
if session := ClientSessionFromContext(ctx); session != nil {
return fmt.Sprintf("%s:%v", session.SessionID(), requestID)
}
return fmt.Sprintf(":%v", requestID)
}
func createResponse(id any, result any) mcp.JSONRPCMessage {
return mcp.NewJSONRPCResultResponse(mcp.NewRequestId(id), result)
}
func createErrorResponse(
id any,
code int,
message string,
) mcp.JSONRPCMessage {
return mcp.JSONRPCError{
JSONRPC: mcp.JSONRPC_VERSION,
ID: mcp.NewRequestId(id),
Error: mcp.NewJSONRPCErrorDetails(code, message, nil),
}
}
//
// Task Request Handlers
//
// handleGetTask handles tasks/get requests to retrieve task status.
func (s *MCPServer) handleGetTask(
ctx context.Context,
id any,
request mcp.GetTaskRequest,
) (*mcp.GetTaskResult, *requestError) {
task, _, err := s.getTask(ctx, request.Params.TaskId)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
result := mcp.NewGetTaskResult(task)
return &result, nil
}
// handleListTasks handles tasks/list requests to list all tasks.
func (s *MCPServer) handleListTasks(
ctx context.Context,
id any,
request mcp.ListTasksRequest,
) (*mcp.ListTasksResult, *requestError) {
tasks := s.listTasks(ctx)
// Sort tasks by TaskId for consistent pagination
sort.Slice(tasks, func(i, j int) bool {
return tasks[i].TaskId < tasks[j].TaskId
})
// Apply pagination
tasksToReturn, nextCursor, err := listByPagination(
ctx,
s,
request.Params.Cursor,
tasks,
)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
result := mcp.ListTasksResult{
Tasks: tasksToReturn,
PaginatedResult: mcp.PaginatedResult{
NextCursor: nextCursor,
},
}
return &result, nil
}
// handleTaskResult handles tasks/result requests to get task results.
func (s *MCPServer) handleTaskResult(
ctx context.Context,
id any,
request mcp.TaskResultRequest,
) (*mcp.TaskResultResult, *requestError) {
task, done, err := s.getTask(ctx, request.Params.TaskId)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
// Wait for task completion if not terminal
if !task.Status.IsTerminal() {
select {
case <-done:
// Task completed
case <-ctx.Done():
return nil, &requestError{
id: id,
code: mcp.REQUEST_INTERRUPTED,
err: ctx.Err(),
}
}
}
// Re-fetch the task entry to get the final result/error under lock
entry, err := s.getTaskEntry(ctx, request.Params.TaskId)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
// Read result and error under lock
s.tasksMu.RLock()
storedResult := entry.result
resultErr := entry.resultErr
taskID := entry.task.TaskId
s.tasksMu.RUnlock()
// Return error if task failed
if resultErr != nil {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: resultErr,
}
}
// Extract the CallToolResult and populate TaskResultResult
result := &mcp.TaskResultResult{
Result: mcp.Result{
Meta: mcp.WithRelatedTask(taskID),
},
}
switch taskResult := storedResult.(type) {
case *mcp.CallToolResult:
result.Content = taskResult.Content
result.StructuredContent = taskResult.StructuredContent
result.IsError = taskResult.IsError
mergeTaskResultMeta(result, taskResult.Meta)
case *mcp.CreateTaskResult:
result.Content = taskResult.Content
result.StructuredContent = taskResult.StructuredContent
result.IsError = taskResult.IsError
mergeTaskResultMeta(result, taskResult.Meta)
}
return result, nil
}
func mergeTaskResultMeta(result *mcp.TaskResultResult, meta *mcp.Meta) {
if meta == nil {
return
}
if result.Meta.AdditionalFields == nil {
result.Meta.AdditionalFields = make(map[string]any)
}
for k, v := range meta.AdditionalFields {
if k != mcp.RelatedTaskMetaKey {
result.Meta.AdditionalFields[k] = v
}
}
}
// handleCancelTask handles tasks/cancel requests to cancel a task.
func (s *MCPServer) handleCancelTask(
ctx context.Context,
id any,
request mcp.CancelTaskRequest,
) (*mcp.CancelTaskResult, *requestError) {
err := s.cancelTask(ctx, request.Params.TaskId)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
// Get the updated task
task, _, err := s.getTask(ctx, request.Params.TaskId)
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INVALID_PARAMS,
err: err,
}
}
result := mcp.NewCancelTaskResult(task)
return &result, nil
}
func (s *MCPServer) handleComplete(
ctx context.Context,
id any,
request mcp.CompleteRequest,
) (*mcp.CompleteResult, *requestError) {
var completion *mcp.Completion
var err error
switch ref := request.Params.Ref.(type) {
case mcp.PromptReference:
completion, err = s.promptCompletionProvider.CompletePromptArgument(
ctx,
ref.Name,
request.Params.Argument,
request.Params.Context,
)
case mcp.ResourceReference:
completion, err = s.resourceCompletionProvider.CompleteResourceArgument(
ctx,
ref.URI,
request.Params.Argument,
request.Params.Context,
)
default:
return nil, &requestError{
id: id,
code: mcp.INVALID_REQUEST,
err: fmt.Errorf("unknown reference type: %v", ref),
}
}
if err != nil {
return nil, &requestError{
id: id,
code: mcp.INTERNAL_ERROR,
err: err,
}
}
// Defensive nil check: default providers always return non-nil completions,
// but custom providers might erroneously return nil. Treat as empty result.
if completion == nil {
return &mcp.CompleteResult{}, nil
}
return &mcp.CompleteResult{
Completion: *completion,
}, nil
}
//
// Task Management Methods
//
// createTask creates a new task entry and returns it.
// Returns an error if the max concurrent tasks limit is exceeded.
func (s *MCPServer) createTask(ctx context.Context, taskID string, toolName string, ttl *int64, pollInterval *int64) (*taskEntry, error) {
// Build task entry first (no lock needed)
opts := []mcp.TaskOption{}
if ttl != nil {
opts = append(opts, mcp.WithTaskTTL(*ttl))
}
if pollInterval != nil {
opts = append(opts, mcp.WithTaskPollInterval(*pollInterval))
}
task := mcp.NewTask(taskID, opts...)
createdAt := time.Now()
entry := &taskEntry{
task: task,
sessionID: getSessionID(ctx),
toolName: toolName,
createdAt: createdAt,
done: make(chan struct{}),
}
// Single critical section for check + increment + insert
s.tasksMu.Lock()
defer s.tasksMu.Unlock()
// Check concurrent task limit
if s.maxConcurrentTasks != nil && *s.maxConcurrentTasks > 0 {
if s.activeTasks >= *s.maxConcurrentTasks {
return nil, fmt.Errorf("max concurrent tasks limit reached (%d)", *s.maxConcurrentTasks)
}
}
// Increment active task counter and insert task atomically
s.activeTasks++
s.tasks[taskID] = entry
// Fire task created hook
if s.taskHooks != nil {
metrics := TaskMetrics{
TaskID: taskID,
ToolName: toolName,
Status: task.Status,
CreatedAt: createdAt,
SessionID: getSessionID(ctx),
}
s.taskHooks.taskCreated(ctx, metrics)
}
// Start TTL cleanup if specified
if ttl != nil && *ttl > 0 {
go s.scheduleTaskCleanup(taskID, *ttl)
}
return entry, nil
}
// getTask retrieves a task by ID, checking session isolation if applicable.
// Returns a copy of the task and the done channel for waiting on completion.
func (s *MCPServer) getTask(ctx context.Context, taskID string) (mcp.Task, chan struct{}, error) {
s.tasksMu.RLock()
entry, exists := s.tasks[taskID]
if !exists {
// Check if this task was recently expired
if _, wasExpired := s.expiredTasks[taskID]; wasExpired {
s.tasksMu.RUnlock()
return mcp.Task{}, nil, fmt.Errorf("task has expired")
}
s.tasksMu.RUnlock()
return mcp.Task{}, nil, fmt.Errorf("task not found")
}
// Verify session isolation
sessionID := getSessionID(ctx)
if entry.sessionID != "" && sessionID != "" && entry.sessionID != sessionID {
s.tasksMu.RUnlock()
return mcp.Task{}, nil, fmt.Errorf("task not found")
}
// Return a copy of the task and the done channel
taskCopy := entry.task
done := entry.done
s.tasksMu.RUnlock()
return taskCopy, done, nil
}
// getTaskEntry retrieves the raw task entry for internal use (requires caller to handle synchronization).
func (s *MCPServer) getTaskEntry(ctx context.Context, taskID string) (*taskEntry, error) {
s.tasksMu.RLock()
entry, exists := s.tasks[taskID]
if !exists {
// Check if this task was recently expired
if _, wasExpired := s.expiredTasks[taskID]; wasExpired {
s.tasksMu.RUnlock()
return nil, fmt.Errorf("task has expired")
}
s.tasksMu.RUnlock()
return nil, fmt.Errorf("task not found")
}
s.tasksMu.RUnlock()
// Verify session isolation
sessionID := getSessionID(ctx)
if entry.sessionID != "" && sessionID != "" && entry.sessionID != sessionID {
return nil, fmt.Errorf("task not found")
}
return entry, nil
}
// listTasks returns copies of all tasks for the current session.
func (s *MCPServer) listTasks(ctx context.Context) []mcp.Task {
sessionID := getSessionID(ctx)
s.tasksMu.RLock()
defer s.tasksMu.RUnlock()
var tasks []mcp.Task
for _, entry := range s.tasks {
// Filter by session if applicable
if sessionID == "" || entry.sessionID == "" || entry.sessionID == sessionID {
tasks = append(tasks, entry.task)
}
}
return tasks
}
// completeTask marks a task as completed with the given result.
func (s *MCPServer) completeTask(entry *taskEntry, result any, err error) {
s.tasksMu.Lock()
defer s.tasksMu.Unlock()
// Guard against double completion
if entry.completed {
return
}
completedAt := time.Now()
duration := completedAt.Sub(entry.createdAt)
if err != nil {
entry.task.Status = mcp.TaskStatusFailed
entry.task.StatusMessage = err.Error()
entry.resultErr = err
} else {
entry.task.Status = mcp.TaskStatusCompleted
entry.result = result
}
// Update the lastUpdatedAt timestamp
entry.task.LastUpdatedAt = completedAt.UTC().Format(time.RFC3339)
// Mark as completed and signal
entry.completed = true
close(entry.done)
// Decrement active tasks counter
s.activeTasks--
// Send task status notification
s.sendTaskStatusNotification(entry.task)
// Fire task hooks
if s.taskHooks != nil {
metrics := TaskMetrics{
TaskID: entry.task.TaskId,
ToolName: entry.toolName,
Status: entry.task.Status,
StatusMessage: entry.task.StatusMessage,
CreatedAt: entry.createdAt,
CompletedAt: &completedAt,
Duration: duration,
SessionID: entry.sessionID,
Error: err,
}
if err != nil {
s.taskHooks.taskFailed(context.Background(), metrics)
} else {
s.taskHooks.taskCompleted(context.Background(), metrics)
}
}
}
// cancelTask cancels a running task.
func (s *MCPServer) cancelTask(ctx context.Context, taskID string) error {
entry, err := s.getTaskEntry(ctx, taskID)
if err != nil {
return err
}
s.tasksMu.Lock()
defer s.tasksMu.Unlock()
// Don't allow cancelling already completed tasks
if entry.completed {
return fmt.Errorf("cannot cancel task in terminal status: %s", entry.task.Status)
}
// Cancel the context if available
if entry.cancelFunc != nil {
entry.cancelFunc()
}
cancelledAt := time.Now()
duration := cancelledAt.Sub(entry.createdAt)
entry.task.Status = mcp.TaskStatusCancelled
entry.task.StatusMessage = "Task cancelled by request"
// Update the lastUpdatedAt timestamp
entry.task.LastUpdatedAt = cancelledAt.UTC().Format(time.RFC3339)
// Mark as completed and signal
entry.completed = true
close(entry.done)
// Decrement active tasks counter
s.activeTasks--
// Send task status notification
s.sendTaskStatusNotification(entry.task)
// Fire task cancellation hook
if s.taskHooks != nil {
metrics := TaskMetrics{
TaskID: entry.task.TaskId,
ToolName: entry.toolName,
Status: entry.task.Status,
StatusMessage: entry.task.StatusMessage,
CreatedAt: entry.createdAt,
CompletedAt: &cancelledAt,
Duration: duration,
SessionID: entry.sessionID,
}
s.taskHooks.taskCancelled(ctx, metrics)
}
return nil
}
// scheduleTaskCleanup schedules a task for cleanup after its TTL expires.
func (s *MCPServer) scheduleTaskCleanup(taskID string, ttlMs int64) {
time.Sleep(time.Duration(ttlMs) * time.Millisecond)
s.tasksMu.Lock()
delete(s.tasks, taskID)
// Record that this task expired for better error messages
// Keep the tombstone for 5 minutes to allow clients to distinguish
// between "not found" and "expired"
s.expiredTasks[taskID] = time.Now()
s.tasksMu.Unlock()
// Clean up the tombstone after 5 minutes
go func() {
time.Sleep(5 * time.Minute)
s.tasksMu.Lock()
delete(s.expiredTasks, taskID)
s.tasksMu.Unlock()
}()
}
// sendTaskStatusNotification sends a notification when a task's status changes.
func (s *MCPServer) sendTaskStatusNotification(task mcp.Task) {
// Convert task to map[string]any for notification params
taskMap := map[string]any{
"taskId": task.TaskId,
"status": task.Status,
"createdAt": task.CreatedAt,
"lastUpdatedAt": task.LastUpdatedAt,
}
if task.StatusMessage != "" {
taskMap["statusMessage"] = task.StatusMessage
}
if task.TTL != nil {
taskMap["ttl"] = *task.TTL
}
if task.PollInterval != nil {
taskMap["pollInterval"] = *task.PollInterval
}
s.SendNotificationToAllClients(mcp.MethodNotificationTasksStatus, taskMap)
}
// getSessionID extracts the session ID from the context.
func getSessionID(ctx context.Context) string {
if session := ClientSessionFromContext(ctx); session != nil {
return session.SessionID()
}
return ""
}