1// Package server provides MCP (Model Context Protocol) server implementations.
2package server
3
4import (
5 "context"
6 "encoding/base64"
7 "encoding/json"
8 "fmt"
9 "slices"
10 "sort"
11 "sync"
12
13 "github.com/mark3labs/mcp-go/mcp"
14)
15
16// resourceEntry holds both a resource and its handler
17type resourceEntry struct {
18 resource mcp.Resource
19 handler ResourceHandlerFunc
20}
21
22// resourceTemplateEntry holds both a template and its handler
23type resourceTemplateEntry struct {
24 template mcp.ResourceTemplate
25 handler ResourceTemplateHandlerFunc
26}
27
28// ServerOption is a function that configures an MCPServer.
29type ServerOption func(*MCPServer)
30
31// ResourceHandlerFunc is a function that returns resource contents.
32type ResourceHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
33
34// ResourceTemplateHandlerFunc is a function that returns a resource template.
35type ResourceTemplateHandlerFunc func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error)
36
37// PromptHandlerFunc handles prompt requests with given arguments.
38type PromptHandlerFunc func(ctx context.Context, request mcp.GetPromptRequest) (*mcp.GetPromptResult, error)
39
40// ToolHandlerFunc handles tool calls with given arguments.
41type ToolHandlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error)
42
43// ToolHandlerMiddleware is a middleware function that wraps a ToolHandlerFunc.
44type ToolHandlerMiddleware func(ToolHandlerFunc) ToolHandlerFunc
45
46// ToolFilterFunc is a function that filters tools based on context, typically using session information.
47type ToolFilterFunc func(ctx context.Context, tools []mcp.Tool) []mcp.Tool
48
49// ServerTool combines a Tool with its ToolHandlerFunc.
50type ServerTool struct {
51 Tool mcp.Tool
52 Handler ToolHandlerFunc
53}
54
55// ServerPrompt combines a Prompt with its handler function.
56type ServerPrompt struct {
57 Prompt mcp.Prompt
58 Handler PromptHandlerFunc
59}
60
61// ServerResource combines a Resource with its handler function.
62type ServerResource struct {
63 Resource mcp.Resource
64 Handler ResourceHandlerFunc
65}
66
67// serverKey is the context key for storing the server instance
68type serverKey struct{}
69
70// ServerFromContext retrieves the MCPServer instance from a context
71func ServerFromContext(ctx context.Context) *MCPServer {
72 if srv, ok := ctx.Value(serverKey{}).(*MCPServer); ok {
73 return srv
74 }
75 return nil
76}
77
78// UnparsableMessageError is attached to the RequestError when json.Unmarshal
79// fails on the request.
80type UnparsableMessageError struct {
81 message json.RawMessage
82 method mcp.MCPMethod
83 err error
84}
85
86func (e *UnparsableMessageError) Error() string {
87 return fmt.Sprintf("unparsable %s request: %s", e.method, e.err)
88}
89
90func (e *UnparsableMessageError) Unwrap() error {
91 return e.err
92}
93
94func (e *UnparsableMessageError) GetMessage() json.RawMessage {
95 return e.message
96}
97
98func (e *UnparsableMessageError) GetMethod() mcp.MCPMethod {
99 return e.method
100}
101
102// RequestError is an error that can be converted to a JSON-RPC error.
103// Implements Unwrap() to allow inspecting the error chain.
104type requestError struct {
105 id any
106 code int
107 err error
108}
109
110func (e *requestError) Error() string {
111 return fmt.Sprintf("request error: %s", e.err)
112}
113
114func (e *requestError) ToJSONRPCError() mcp.JSONRPCError {
115 return mcp.JSONRPCError{
116 JSONRPC: mcp.JSONRPC_VERSION,
117 ID: mcp.NewRequestId(e.id),
118 Error: struct {
119 Code int `json:"code"`
120 Message string `json:"message"`
121 Data any `json:"data,omitempty"`
122 }{
123 Code: e.code,
124 Message: e.err.Error(),
125 },
126 }
127}
128
129func (e *requestError) Unwrap() error {
130 return e.err
131}
132
133// NotificationHandlerFunc handles incoming notifications.
134type NotificationHandlerFunc func(ctx context.Context, notification mcp.JSONRPCNotification)
135
136// MCPServer implements a Model Context Protocol server that can handle various types of requests
137// including resources, prompts, and tools.
138type MCPServer struct {
139 // Separate mutexes for different resource types
140 resourcesMu sync.RWMutex
141 promptsMu sync.RWMutex
142 toolsMu sync.RWMutex
143 middlewareMu sync.RWMutex
144 notificationHandlersMu sync.RWMutex
145 capabilitiesMu sync.RWMutex
146 toolFiltersMu sync.RWMutex
147
148 name string
149 version string
150 instructions string
151 resources map[string]resourceEntry
152 resourceTemplates map[string]resourceTemplateEntry
153 prompts map[string]mcp.Prompt
154 promptHandlers map[string]PromptHandlerFunc
155 tools map[string]ServerTool
156 toolHandlerMiddlewares []ToolHandlerMiddleware
157 toolFilters []ToolFilterFunc
158 notificationHandlers map[string]NotificationHandlerFunc
159 capabilities serverCapabilities
160 paginationLimit *int
161 sessions sync.Map
162 hooks *Hooks
163}
164
165// WithPaginationLimit sets the pagination limit for the server.
166func WithPaginationLimit(limit int) ServerOption {
167 return func(s *MCPServer) {
168 s.paginationLimit = &limit
169 }
170}
171
172// serverCapabilities defines the supported features of the MCP server
173type serverCapabilities struct {
174 tools *toolCapabilities
175 resources *resourceCapabilities
176 prompts *promptCapabilities
177 logging *bool
178}
179
180// resourceCapabilities defines the supported resource-related features
181type resourceCapabilities struct {
182 subscribe bool
183 listChanged bool
184}
185
186// promptCapabilities defines the supported prompt-related features
187type promptCapabilities struct {
188 listChanged bool
189}
190
191// toolCapabilities defines the supported tool-related features
192type toolCapabilities struct {
193 listChanged bool
194}
195
196// WithResourceCapabilities configures resource-related server capabilities
197func WithResourceCapabilities(subscribe, listChanged bool) ServerOption {
198 return func(s *MCPServer) {
199 // Always create a non-nil capability object
200 s.capabilities.resources = &resourceCapabilities{
201 subscribe: subscribe,
202 listChanged: listChanged,
203 }
204 }
205}
206
207// WithToolHandlerMiddleware allows adding a middleware for the
208// tool handler call chain.
209func WithToolHandlerMiddleware(
210 toolHandlerMiddleware ToolHandlerMiddleware,
211) ServerOption {
212 return func(s *MCPServer) {
213 s.middlewareMu.Lock()
214 s.toolHandlerMiddlewares = append(s.toolHandlerMiddlewares, toolHandlerMiddleware)
215 s.middlewareMu.Unlock()
216 }
217}
218
219// WithToolFilter adds a filter function that will be applied to tools before they are returned in list_tools
220func WithToolFilter(
221 toolFilter ToolFilterFunc,
222) ServerOption {
223 return func(s *MCPServer) {
224 s.toolFiltersMu.Lock()
225 s.toolFilters = append(s.toolFilters, toolFilter)
226 s.toolFiltersMu.Unlock()
227 }
228}
229
230// WithRecovery adds a middleware that recovers from panics in tool handlers.
231func WithRecovery() ServerOption {
232 return WithToolHandlerMiddleware(func(next ToolHandlerFunc) ToolHandlerFunc {
233 return func(ctx context.Context, request mcp.CallToolRequest) (result *mcp.CallToolResult, err error) {
234 defer func() {
235 if r := recover(); r != nil {
236 err = fmt.Errorf(
237 "panic recovered in %s tool handler: %v",
238 request.Params.Name,
239 r,
240 )
241 }
242 }()
243 return next(ctx, request)
244 }
245 })
246}
247
248// WithHooks allows adding hooks that will be called before or after
249// either [all] requests or before / after specific request methods, or else
250// prior to returning an error to the client.
251func WithHooks(hooks *Hooks) ServerOption {
252 return func(s *MCPServer) {
253 s.hooks = hooks
254 }
255}
256
257// WithPromptCapabilities configures prompt-related server capabilities
258func WithPromptCapabilities(listChanged bool) ServerOption {
259 return func(s *MCPServer) {
260 // Always create a non-nil capability object
261 s.capabilities.prompts = &promptCapabilities{
262 listChanged: listChanged,
263 }
264 }
265}
266
267// WithToolCapabilities configures tool-related server capabilities
268func WithToolCapabilities(listChanged bool) ServerOption {
269 return func(s *MCPServer) {
270 // Always create a non-nil capability object
271 s.capabilities.tools = &toolCapabilities{
272 listChanged: listChanged,
273 }
274 }
275}
276
277// WithLogging enables logging capabilities for the server
278func WithLogging() ServerOption {
279 return func(s *MCPServer) {
280 s.capabilities.logging = mcp.ToBoolPtr(true)
281 }
282}
283
284// WithInstructions sets the server instructions for the client returned in the initialize response
285func WithInstructions(instructions string) ServerOption {
286 return func(s *MCPServer) {
287 s.instructions = instructions
288 }
289}
290
291// NewMCPServer creates a new MCP server instance with the given name, version and options
292func NewMCPServer(
293 name, version string,
294 opts ...ServerOption,
295) *MCPServer {
296 s := &MCPServer{
297 resources: make(map[string]resourceEntry),
298 resourceTemplates: make(map[string]resourceTemplateEntry),
299 prompts: make(map[string]mcp.Prompt),
300 promptHandlers: make(map[string]PromptHandlerFunc),
301 tools: make(map[string]ServerTool),
302 name: name,
303 version: version,
304 notificationHandlers: make(map[string]NotificationHandlerFunc),
305 capabilities: serverCapabilities{
306 tools: nil,
307 resources: nil,
308 prompts: nil,
309 logging: nil,
310 },
311 }
312
313 for _, opt := range opts {
314 opt(s)
315 }
316
317 return s
318}
319
320// AddResources registers multiple resources at once
321func (s *MCPServer) AddResources(resources ...ServerResource) {
322 s.implicitlyRegisterResourceCapabilities()
323
324 s.resourcesMu.Lock()
325 for _, entry := range resources {
326 s.resources[entry.Resource.URI] = resourceEntry{
327 resource: entry.Resource,
328 handler: entry.Handler,
329 }
330 }
331 s.resourcesMu.Unlock()
332
333 // When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification
334 if s.capabilities.resources.listChanged {
335 // Send notification to all initialized sessions
336 s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil)
337 }
338}
339
340// AddResource registers a new resource and its handler
341func (s *MCPServer) AddResource(
342 resource mcp.Resource,
343 handler ResourceHandlerFunc,
344) {
345 s.AddResources(ServerResource{Resource: resource, Handler: handler})
346}
347
348// RemoveResource removes a resource from the server
349func (s *MCPServer) RemoveResource(uri string) {
350 s.resourcesMu.Lock()
351 _, exists := s.resources[uri]
352 if exists {
353 delete(s.resources, uri)
354 }
355 s.resourcesMu.Unlock()
356
357 // Send notification to all initialized sessions if listChanged capability is enabled and we actually remove a resource
358 if exists && s.capabilities.resources != nil && s.capabilities.resources.listChanged {
359 s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil)
360 }
361}
362
363// AddResourceTemplate registers a new resource template and its handler
364func (s *MCPServer) AddResourceTemplate(
365 template mcp.ResourceTemplate,
366 handler ResourceTemplateHandlerFunc,
367) {
368 s.implicitlyRegisterResourceCapabilities()
369
370 s.resourcesMu.Lock()
371 s.resourceTemplates[template.URITemplate.Raw()] = resourceTemplateEntry{
372 template: template,
373 handler: handler,
374 }
375 s.resourcesMu.Unlock()
376
377 // When the list of available resources changes, servers that declared the listChanged capability SHOULD send a notification
378 if s.capabilities.resources.listChanged {
379 // Send notification to all initialized sessions
380 s.SendNotificationToAllClients(mcp.MethodNotificationResourcesListChanged, nil)
381 }
382}
383
384// AddPrompts registers multiple prompts at once
385func (s *MCPServer) AddPrompts(prompts ...ServerPrompt) {
386 s.implicitlyRegisterPromptCapabilities()
387
388 s.promptsMu.Lock()
389 for _, entry := range prompts {
390 s.prompts[entry.Prompt.Name] = entry.Prompt
391 s.promptHandlers[entry.Prompt.Name] = entry.Handler
392 }
393 s.promptsMu.Unlock()
394
395 // When the list of available prompts changes, servers that declared the listChanged capability SHOULD send a notification.
396 if s.capabilities.prompts.listChanged {
397 // Send notification to all initialized sessions
398 s.SendNotificationToAllClients(mcp.MethodNotificationPromptsListChanged, nil)
399 }
400}
401
402// AddPrompt registers a new prompt handler with the given name
403func (s *MCPServer) AddPrompt(prompt mcp.Prompt, handler PromptHandlerFunc) {
404 s.AddPrompts(ServerPrompt{Prompt: prompt, Handler: handler})
405}
406
407// DeletePrompts removes prompts from the server
408func (s *MCPServer) DeletePrompts(names ...string) {
409 s.promptsMu.Lock()
410 var exists bool
411 for _, name := range names {
412 if _, ok := s.prompts[name]; ok {
413 delete(s.prompts, name)
414 delete(s.promptHandlers, name)
415 exists = true
416 }
417 }
418 s.promptsMu.Unlock()
419
420 // Send notification to all initialized sessions if listChanged capability is enabled, and we actually remove a prompt
421 if exists && s.capabilities.prompts != nil && s.capabilities.prompts.listChanged {
422 // Send notification to all initialized sessions
423 s.SendNotificationToAllClients(mcp.MethodNotificationPromptsListChanged, nil)
424 }
425}
426
427// AddTool registers a new tool and its handler
428func (s *MCPServer) AddTool(tool mcp.Tool, handler ToolHandlerFunc) {
429 s.AddTools(ServerTool{Tool: tool, Handler: handler})
430}
431
432// Register tool capabilities due to a tool being added. Default to
433// listChanged: true, but don't change the value if we've already explicitly
434// registered tools.listChanged false.
435func (s *MCPServer) implicitlyRegisterToolCapabilities() {
436 s.implicitlyRegisterCapabilities(
437 func() bool { return s.capabilities.tools != nil },
438 func() { s.capabilities.tools = &toolCapabilities{listChanged: true} },
439 )
440}
441
442func (s *MCPServer) implicitlyRegisterResourceCapabilities() {
443 s.implicitlyRegisterCapabilities(
444 func() bool { return s.capabilities.resources != nil },
445 func() { s.capabilities.resources = &resourceCapabilities{} },
446 )
447}
448
449func (s *MCPServer) implicitlyRegisterPromptCapabilities() {
450 s.implicitlyRegisterCapabilities(
451 func() bool { return s.capabilities.prompts != nil },
452 func() { s.capabilities.prompts = &promptCapabilities{} },
453 )
454}
455
456func (s *MCPServer) implicitlyRegisterCapabilities(check func() bool, register func()) {
457 s.capabilitiesMu.RLock()
458 if check() {
459 s.capabilitiesMu.RUnlock()
460 return
461 }
462 s.capabilitiesMu.RUnlock()
463
464 s.capabilitiesMu.Lock()
465 if !check() {
466 register()
467 }
468 s.capabilitiesMu.Unlock()
469}
470
471// AddTools registers multiple tools at once
472func (s *MCPServer) AddTools(tools ...ServerTool) {
473 s.implicitlyRegisterToolCapabilities()
474
475 s.toolsMu.Lock()
476 for _, entry := range tools {
477 s.tools[entry.Tool.Name] = entry
478 }
479 s.toolsMu.Unlock()
480
481 // When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification.
482 if s.capabilities.tools.listChanged {
483 // Send notification to all initialized sessions
484 s.SendNotificationToAllClients(mcp.MethodNotificationToolsListChanged, nil)
485 }
486}
487
488// SetTools replaces all existing tools with the provided list
489func (s *MCPServer) SetTools(tools ...ServerTool) {
490 s.toolsMu.Lock()
491 s.tools = make(map[string]ServerTool, len(tools))
492 s.toolsMu.Unlock()
493 s.AddTools(tools...)
494}
495
496// DeleteTools removes tools from the server
497func (s *MCPServer) DeleteTools(names ...string) {
498 s.toolsMu.Lock()
499 var exists bool
500 for _, name := range names {
501 if _, ok := s.tools[name]; ok {
502 delete(s.tools, name)
503 exists = true
504 }
505 }
506 s.toolsMu.Unlock()
507
508 // When the list of available tools changes, servers that declared the listChanged capability SHOULD send a notification.
509 if exists && s.capabilities.tools != nil && s.capabilities.tools.listChanged {
510 // Send notification to all initialized sessions
511 s.SendNotificationToAllClients(mcp.MethodNotificationToolsListChanged, nil)
512 }
513}
514
515// AddNotificationHandler registers a new handler for incoming notifications
516func (s *MCPServer) AddNotificationHandler(
517 method string,
518 handler NotificationHandlerFunc,
519) {
520 s.notificationHandlersMu.Lock()
521 defer s.notificationHandlersMu.Unlock()
522 s.notificationHandlers[method] = handler
523}
524
525func (s *MCPServer) handleInitialize(
526 ctx context.Context,
527 _ any,
528 request mcp.InitializeRequest,
529) (*mcp.InitializeResult, *requestError) {
530 capabilities := mcp.ServerCapabilities{}
531
532 // Only add resource capabilities if they're configured
533 if s.capabilities.resources != nil {
534 capabilities.Resources = &struct {
535 Subscribe bool `json:"subscribe,omitempty"`
536 ListChanged bool `json:"listChanged,omitempty"`
537 }{
538 Subscribe: s.capabilities.resources.subscribe,
539 ListChanged: s.capabilities.resources.listChanged,
540 }
541 }
542
543 // Only add prompt capabilities if they're configured
544 if s.capabilities.prompts != nil {
545 capabilities.Prompts = &struct {
546 ListChanged bool `json:"listChanged,omitempty"`
547 }{
548 ListChanged: s.capabilities.prompts.listChanged,
549 }
550 }
551
552 // Only add tool capabilities if they're configured
553 if s.capabilities.tools != nil {
554 capabilities.Tools = &struct {
555 ListChanged bool `json:"listChanged,omitempty"`
556 }{
557 ListChanged: s.capabilities.tools.listChanged,
558 }
559 }
560
561 if s.capabilities.logging != nil && *s.capabilities.logging {
562 capabilities.Logging = &struct{}{}
563 }
564
565 result := mcp.InitializeResult{
566 ProtocolVersion: s.protocolVersion(request.Params.ProtocolVersion),
567 ServerInfo: mcp.Implementation{
568 Name: s.name,
569 Version: s.version,
570 },
571 Capabilities: capabilities,
572 Instructions: s.instructions,
573 }
574
575 if session := ClientSessionFromContext(ctx); session != nil {
576 session.Initialize()
577
578 // Store client info if the session supports it
579 if sessionWithClientInfo, ok := session.(SessionWithClientInfo); ok {
580 sessionWithClientInfo.SetClientInfo(request.Params.ClientInfo)
581 }
582 }
583 return &result, nil
584}
585
586func (s *MCPServer) protocolVersion(clientVersion string) string {
587 if slices.Contains(mcp.ValidProtocolVersions, clientVersion) {
588 return clientVersion
589 }
590
591 return mcp.LATEST_PROTOCOL_VERSION
592}
593
594func (s *MCPServer) handlePing(
595 _ context.Context,
596 _ any,
597 _ mcp.PingRequest,
598) (*mcp.EmptyResult, *requestError) {
599 return &mcp.EmptyResult{}, nil
600}
601
602func (s *MCPServer) handleSetLevel(
603 ctx context.Context,
604 id any,
605 request mcp.SetLevelRequest,
606) (*mcp.EmptyResult, *requestError) {
607 clientSession := ClientSessionFromContext(ctx)
608 if clientSession == nil || !clientSession.Initialized() {
609 return nil, &requestError{
610 id: id,
611 code: mcp.INTERNAL_ERROR,
612 err: ErrSessionNotInitialized,
613 }
614 }
615
616 sessionLogging, ok := clientSession.(SessionWithLogging)
617 if !ok {
618 return nil, &requestError{
619 id: id,
620 code: mcp.INTERNAL_ERROR,
621 err: ErrSessionDoesNotSupportLogging,
622 }
623 }
624
625 level := request.Params.Level
626 // Validate logging level
627 switch level {
628 case mcp.LoggingLevelDebug, mcp.LoggingLevelInfo, mcp.LoggingLevelNotice,
629 mcp.LoggingLevelWarning, mcp.LoggingLevelError, mcp.LoggingLevelCritical,
630 mcp.LoggingLevelAlert, mcp.LoggingLevelEmergency:
631 // Valid level
632 default:
633 return nil, &requestError{
634 id: id,
635 code: mcp.INVALID_PARAMS,
636 err: fmt.Errorf("invalid logging level '%s'", level),
637 }
638 }
639
640 sessionLogging.SetLogLevel(level)
641
642 return &mcp.EmptyResult{}, nil
643}
644
645func listByPagination[T mcp.Named](
646 _ context.Context,
647 s *MCPServer,
648 cursor mcp.Cursor,
649 allElements []T,
650) ([]T, mcp.Cursor, error) {
651 startPos := 0
652 if cursor != "" {
653 c, err := base64.StdEncoding.DecodeString(string(cursor))
654 if err != nil {
655 return nil, "", err
656 }
657 cString := string(c)
658 startPos = sort.Search(len(allElements), func(i int) bool {
659 return allElements[i].GetName() > cString
660 })
661 }
662 endPos := len(allElements)
663 if s.paginationLimit != nil {
664 if len(allElements) > startPos+*s.paginationLimit {
665 endPos = startPos + *s.paginationLimit
666 }
667 }
668 elementsToReturn := allElements[startPos:endPos]
669 // set the next cursor
670 nextCursor := func() mcp.Cursor {
671 if s.paginationLimit != nil && len(elementsToReturn) >= *s.paginationLimit {
672 nc := elementsToReturn[len(elementsToReturn)-1].GetName()
673 toString := base64.StdEncoding.EncodeToString([]byte(nc))
674 return mcp.Cursor(toString)
675 }
676 return ""
677 }()
678 return elementsToReturn, nextCursor, nil
679}
680
681func (s *MCPServer) handleListResources(
682 ctx context.Context,
683 id any,
684 request mcp.ListResourcesRequest,
685) (*mcp.ListResourcesResult, *requestError) {
686 s.resourcesMu.RLock()
687 resources := make([]mcp.Resource, 0, len(s.resources))
688 for _, entry := range s.resources {
689 resources = append(resources, entry.resource)
690 }
691 s.resourcesMu.RUnlock()
692
693 // Sort the resources by name
694 sort.Slice(resources, func(i, j int) bool {
695 return resources[i].Name < resources[j].Name
696 })
697 resourcesToReturn, nextCursor, err := listByPagination(
698 ctx,
699 s,
700 request.Params.Cursor,
701 resources,
702 )
703 if err != nil {
704 return nil, &requestError{
705 id: id,
706 code: mcp.INVALID_PARAMS,
707 err: err,
708 }
709 }
710 result := mcp.ListResourcesResult{
711 Resources: resourcesToReturn,
712 PaginatedResult: mcp.PaginatedResult{
713 NextCursor: nextCursor,
714 },
715 }
716 return &result, nil
717}
718
719func (s *MCPServer) handleListResourceTemplates(
720 ctx context.Context,
721 id any,
722 request mcp.ListResourceTemplatesRequest,
723) (*mcp.ListResourceTemplatesResult, *requestError) {
724 s.resourcesMu.RLock()
725 templates := make([]mcp.ResourceTemplate, 0, len(s.resourceTemplates))
726 for _, entry := range s.resourceTemplates {
727 templates = append(templates, entry.template)
728 }
729 s.resourcesMu.RUnlock()
730 sort.Slice(templates, func(i, j int) bool {
731 return templates[i].Name < templates[j].Name
732 })
733 templatesToReturn, nextCursor, err := listByPagination(
734 ctx,
735 s,
736 request.Params.Cursor,
737 templates,
738 )
739 if err != nil {
740 return nil, &requestError{
741 id: id,
742 code: mcp.INVALID_PARAMS,
743 err: err,
744 }
745 }
746 result := mcp.ListResourceTemplatesResult{
747 ResourceTemplates: templatesToReturn,
748 PaginatedResult: mcp.PaginatedResult{
749 NextCursor: nextCursor,
750 },
751 }
752 return &result, nil
753}
754
755func (s *MCPServer) handleReadResource(
756 ctx context.Context,
757 id any,
758 request mcp.ReadResourceRequest,
759) (*mcp.ReadResourceResult, *requestError) {
760 s.resourcesMu.RLock()
761 // First try direct resource handlers
762 if entry, ok := s.resources[request.Params.URI]; ok {
763 handler := entry.handler
764 s.resourcesMu.RUnlock()
765 contents, err := handler(ctx, request)
766 if err != nil {
767 return nil, &requestError{
768 id: id,
769 code: mcp.INTERNAL_ERROR,
770 err: err,
771 }
772 }
773 return &mcp.ReadResourceResult{Contents: contents}, nil
774 }
775
776 // If no direct handler found, try matching against templates
777 var matchedHandler ResourceTemplateHandlerFunc
778 var matched bool
779 for _, entry := range s.resourceTemplates {
780 template := entry.template
781 if matchesTemplate(request.Params.URI, template.URITemplate) {
782 matchedHandler = entry.handler
783 matched = true
784 matchedVars := template.URITemplate.Match(request.Params.URI)
785 // Convert matched variables to a map
786 request.Params.Arguments = make(map[string]any, len(matchedVars))
787 for name, value := range matchedVars {
788 request.Params.Arguments[name] = value.V
789 }
790 break
791 }
792 }
793 s.resourcesMu.RUnlock()
794
795 if matched {
796 contents, err := matchedHandler(ctx, request)
797 if err != nil {
798 return nil, &requestError{
799 id: id,
800 code: mcp.INTERNAL_ERROR,
801 err: err,
802 }
803 }
804 return &mcp.ReadResourceResult{Contents: contents}, nil
805 }
806
807 return nil, &requestError{
808 id: id,
809 code: mcp.RESOURCE_NOT_FOUND,
810 err: fmt.Errorf(
811 "handler not found for resource URI '%s': %w",
812 request.Params.URI,
813 ErrResourceNotFound,
814 ),
815 }
816}
817
818// matchesTemplate checks if a URI matches a URI template pattern
819func matchesTemplate(uri string, template *mcp.URITemplate) bool {
820 return template.Regexp().MatchString(uri)
821}
822
823func (s *MCPServer) handleListPrompts(
824 ctx context.Context,
825 id any,
826 request mcp.ListPromptsRequest,
827) (*mcp.ListPromptsResult, *requestError) {
828 s.promptsMu.RLock()
829 prompts := make([]mcp.Prompt, 0, len(s.prompts))
830 for _, prompt := range s.prompts {
831 prompts = append(prompts, prompt)
832 }
833 s.promptsMu.RUnlock()
834
835 // sort prompts by name
836 sort.Slice(prompts, func(i, j int) bool {
837 return prompts[i].Name < prompts[j].Name
838 })
839 promptsToReturn, nextCursor, err := listByPagination(
840 ctx,
841 s,
842 request.Params.Cursor,
843 prompts,
844 )
845 if err != nil {
846 return nil, &requestError{
847 id: id,
848 code: mcp.INVALID_PARAMS,
849 err: err,
850 }
851 }
852 result := mcp.ListPromptsResult{
853 Prompts: promptsToReturn,
854 PaginatedResult: mcp.PaginatedResult{
855 NextCursor: nextCursor,
856 },
857 }
858 return &result, nil
859}
860
861func (s *MCPServer) handleGetPrompt(
862 ctx context.Context,
863 id any,
864 request mcp.GetPromptRequest,
865) (*mcp.GetPromptResult, *requestError) {
866 s.promptsMu.RLock()
867 handler, ok := s.promptHandlers[request.Params.Name]
868 s.promptsMu.RUnlock()
869
870 if !ok {
871 return nil, &requestError{
872 id: id,
873 code: mcp.INVALID_PARAMS,
874 err: fmt.Errorf("prompt '%s' not found: %w", request.Params.Name, ErrPromptNotFound),
875 }
876 }
877
878 result, err := handler(ctx, request)
879 if err != nil {
880 return nil, &requestError{
881 id: id,
882 code: mcp.INTERNAL_ERROR,
883 err: err,
884 }
885 }
886
887 return result, nil
888}
889
890func (s *MCPServer) handleListTools(
891 ctx context.Context,
892 id any,
893 request mcp.ListToolsRequest,
894) (*mcp.ListToolsResult, *requestError) {
895 // Get the base tools from the server
896 s.toolsMu.RLock()
897 tools := make([]mcp.Tool, 0, len(s.tools))
898
899 // Get all tool names for consistent ordering
900 toolNames := make([]string, 0, len(s.tools))
901 for name := range s.tools {
902 toolNames = append(toolNames, name)
903 }
904
905 // Sort the tool names for consistent ordering
906 sort.Strings(toolNames)
907
908 // Add tools in sorted order
909 for _, name := range toolNames {
910 tools = append(tools, s.tools[name].Tool)
911 }
912 s.toolsMu.RUnlock()
913
914 // Check if there are session-specific tools
915 session := ClientSessionFromContext(ctx)
916 if session != nil {
917 if sessionWithTools, ok := session.(SessionWithTools); ok {
918 if sessionTools := sessionWithTools.GetSessionTools(); sessionTools != nil {
919 // Override or add session-specific tools
920 // We need to create a map first to merge the tools properly
921 toolMap := make(map[string]mcp.Tool)
922
923 // Add global tools first
924 for _, tool := range tools {
925 toolMap[tool.Name] = tool
926 }
927
928 // Then override with session-specific tools
929 for name, serverTool := range sessionTools {
930 toolMap[name] = serverTool.Tool
931 }
932
933 // Convert back to slice
934 tools = make([]mcp.Tool, 0, len(toolMap))
935 for _, tool := range toolMap {
936 tools = append(tools, tool)
937 }
938
939 // Sort again to maintain consistent ordering
940 sort.Slice(tools, func(i, j int) bool {
941 return tools[i].Name < tools[j].Name
942 })
943 }
944 }
945 }
946
947 // Apply tool filters if any are defined
948 s.toolFiltersMu.RLock()
949 if len(s.toolFilters) > 0 {
950 for _, filter := range s.toolFilters {
951 tools = filter(ctx, tools)
952 }
953 }
954 s.toolFiltersMu.RUnlock()
955
956 // Apply pagination
957 toolsToReturn, nextCursor, err := listByPagination(
958 ctx,
959 s,
960 request.Params.Cursor,
961 tools,
962 )
963 if err != nil {
964 return nil, &requestError{
965 id: id,
966 code: mcp.INVALID_PARAMS,
967 err: err,
968 }
969 }
970
971 result := mcp.ListToolsResult{
972 Tools: toolsToReturn,
973 PaginatedResult: mcp.PaginatedResult{
974 NextCursor: nextCursor,
975 },
976 }
977 return &result, nil
978}
979
980func (s *MCPServer) handleToolCall(
981 ctx context.Context,
982 id any,
983 request mcp.CallToolRequest,
984) (*mcp.CallToolResult, *requestError) {
985 // First check session-specific tools
986 var tool ServerTool
987 var ok bool
988
989 session := ClientSessionFromContext(ctx)
990 if session != nil {
991 if sessionWithTools, typeAssertOk := session.(SessionWithTools); typeAssertOk {
992 if sessionTools := sessionWithTools.GetSessionTools(); sessionTools != nil {
993 var sessionOk bool
994 tool, sessionOk = sessionTools[request.Params.Name]
995 if sessionOk {
996 ok = true
997 }
998 }
999 }
1000 }
1001
1002 // If not found in session tools, check global tools
1003 if !ok {
1004 s.toolsMu.RLock()
1005 tool, ok = s.tools[request.Params.Name]
1006 s.toolsMu.RUnlock()
1007 }
1008
1009 if !ok {
1010 return nil, &requestError{
1011 id: id,
1012 code: mcp.INVALID_PARAMS,
1013 err: fmt.Errorf("tool '%s' not found: %w", request.Params.Name, ErrToolNotFound),
1014 }
1015 }
1016
1017 finalHandler := tool.Handler
1018
1019 s.middlewareMu.RLock()
1020 mw := s.toolHandlerMiddlewares
1021 s.middlewareMu.RUnlock()
1022
1023 // Apply middlewares in reverse order
1024 for i := len(mw) - 1; i >= 0; i-- {
1025 finalHandler = mw[i](finalHandler)
1026 }
1027
1028 result, err := finalHandler(ctx, request)
1029 if err != nil {
1030 return nil, &requestError{
1031 id: id,
1032 code: mcp.INTERNAL_ERROR,
1033 err: err,
1034 }
1035 }
1036
1037 return result, nil
1038}
1039
1040func (s *MCPServer) handleNotification(
1041 ctx context.Context,
1042 notification mcp.JSONRPCNotification,
1043) mcp.JSONRPCMessage {
1044 s.notificationHandlersMu.RLock()
1045 handler, ok := s.notificationHandlers[notification.Method]
1046 s.notificationHandlersMu.RUnlock()
1047
1048 if ok {
1049 handler(ctx, notification)
1050 }
1051 return nil
1052}
1053
1054func createResponse(id any, result any) mcp.JSONRPCMessage {
1055 return mcp.JSONRPCResponse{
1056 JSONRPC: mcp.JSONRPC_VERSION,
1057 ID: mcp.NewRequestId(id),
1058 Result: result,
1059 }
1060}
1061
1062func createErrorResponse(
1063 id any,
1064 code int,
1065 message string,
1066) mcp.JSONRPCMessage {
1067 return mcp.JSONRPCError{
1068 JSONRPC: mcp.JSONRPC_VERSION,
1069 ID: mcp.NewRequestId(id),
1070 Error: struct {
1071 Code int `json:"code"`
1072 Message string `json:"message"`
1073 Data any `json:"data,omitempty"`
1074 }{
1075 Code: code,
1076 Message: message,
1077 },
1078 }
1079}