mirror of
https://github.com/openshift/openshift-mcp-server.git
synced 2025-10-17 14:27:48 +03:00
1239 lines
34 KiB
Go
1239 lines
34 KiB
Go
// Package server provides MCP (Model Context Protocol) server implementations.
|
|
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"sync"
|
|
|
|
"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
|
|
}
|
|
|
|
// 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)
|
|
|
|
// 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
|
|
|
|
// ServerTool combines a Tool with its ToolHandlerFunc.
|
|
type ServerTool struct {
|
|
Tool mcp.Tool
|
|
Handler ToolHandlerFunc
|
|
}
|
|
|
|
// 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: struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data any `json:"data,omitempty"`
|
|
}{
|
|
Code: e.code,
|
|
Message: e.err.Error(),
|
|
},
|
|
}
|
|
}
|
|
|
|
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
|
|
notificationHandlersMu sync.RWMutex
|
|
capabilitiesMu sync.RWMutex
|
|
toolFiltersMu sync.RWMutex
|
|
|
|
name string
|
|
version string
|
|
instructions string
|
|
resources map[string]resourceEntry
|
|
resourceTemplates map[string]resourceTemplateEntry
|
|
prompts map[string]mcp.Prompt
|
|
promptHandlers map[string]PromptHandlerFunc
|
|
tools map[string]ServerTool
|
|
toolHandlerMiddlewares []ToolHandlerMiddleware
|
|
resourceHandlerMiddlewares []ResourceHandlerMiddleware
|
|
toolFilters []ToolFilterFunc
|
|
notificationHandlers map[string]NotificationHandlerFunc
|
|
capabilities serverCapabilities
|
|
paginationLimit *int
|
|
sessions sync.Map
|
|
hooks *Hooks
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
}
|
|
|
|
// 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),
|
|
toolHandlerMiddlewares: make([]ToolHandlerMiddleware, 0),
|
|
resourceHandlerMiddlewares: make([]ResourceHandlerMiddleware, 0),
|
|
name: name,
|
|
version: version,
|
|
notificationHandlers: make(map[string]NotificationHandlerFunc),
|
|
capabilities: serverCapabilities{
|
|
tools: nil,
|
|
resources: nil,
|
|
prompts: nil,
|
|
logging: 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})
|
|
}
|
|
|
|
// 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 {
|
|
s.tools[entry.Tool.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 = &struct{}{}
|
|
}
|
|
|
|
result := mcp.InitializeResult{
|
|
ProtocolVersion: s.protocolVersion(request.Params.ProtocolVersion),
|
|
ServerInfo: mcp.Implementation{
|
|
Name: s.name,
|
|
Version: s.version,
|
|
},
|
|
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()
|
|
resources := make([]mcp.Resource, 0, len(s.resources))
|
|
for _, entry := range s.resources {
|
|
resources = append(resources, entry.resource)
|
|
}
|
|
s.resourcesMu.RUnlock()
|
|
|
|
// Sort the resources by name
|
|
sort.Slice(resources, func(i, j int) bool {
|
|
return resources[i].Name < resources[j].Name
|
|
})
|
|
resourcesToReturn, nextCursor, err := listByPagination(
|
|
ctx,
|
|
s,
|
|
request.Params.Cursor,
|
|
resources,
|
|
)
|
|
if err != nil {
|
|
return nil, &requestError{
|
|
id: id,
|
|
code: mcp.INVALID_PARAMS,
|
|
err: err,
|
|
}
|
|
}
|
|
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) {
|
|
s.resourcesMu.RLock()
|
|
templates := make([]mcp.ResourceTemplate, 0, len(s.resourceTemplates))
|
|
for _, entry := range s.resourceTemplates {
|
|
templates = append(templates, entry.template)
|
|
}
|
|
s.resourcesMu.RUnlock()
|
|
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 try direct resource handlers
|
|
if entry, ok := s.resources[request.Params.URI]; ok {
|
|
handler := entry.handler
|
|
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
|
|
for _, entry := range s.resourceTemplates {
|
|
template := entry.template
|
|
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 {
|
|
contents, err := matchedHandler(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
|
|
})
|
|
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),
|
|
}
|
|
}
|
|
|
|
result, err := handler(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
|
|
s.toolsMu.RLock()
|
|
tools := make([]mcp.Tool, 0, len(s.tools))
|
|
|
|
// Get all tool names for consistent ordering
|
|
toolNames := make([]string, 0, len(s.tools))
|
|
for name := range s.tools {
|
|
toolNames = append(toolNames, name)
|
|
}
|
|
|
|
// Sort the tool names for consistent ordering
|
|
sort.Strings(toolNames)
|
|
|
|
// Add tools in sorted order
|
|
for _, name := range toolNames {
|
|
tools = append(tools, s.tools[name].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,
|
|
) (*mcp.CallToolResult, *requestError) {
|
|
// First check session-specific tools
|
|
var tool ServerTool
|
|
var ok 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]
|
|
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),
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (s *MCPServer) handleNotification(
|
|
ctx context.Context,
|
|
notification mcp.JSONRPCNotification,
|
|
) mcp.JSONRPCMessage {
|
|
s.notificationHandlersMu.RLock()
|
|
handler, ok := s.notificationHandlers[notification.Method]
|
|
s.notificationHandlersMu.RUnlock()
|
|
|
|
if ok {
|
|
handler(ctx, notification)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func createResponse(id any, result any) mcp.JSONRPCMessage {
|
|
return mcp.JSONRPCResponse{
|
|
JSONRPC: mcp.JSONRPC_VERSION,
|
|
ID: mcp.NewRequestId(id),
|
|
Result: result,
|
|
}
|
|
}
|
|
|
|
func createErrorResponse(
|
|
id any,
|
|
code int,
|
|
message string,
|
|
) mcp.JSONRPCMessage {
|
|
return mcp.JSONRPCError{
|
|
JSONRPC: mcp.JSONRPC_VERSION,
|
|
ID: mcp.NewRequestId(id),
|
|
Error: struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
Data any `json:"data,omitempty"`
|
|
}{
|
|
Code: code,
|
|
Message: message,
|
|
},
|
|
}
|
|
}
|