@@ -12,7 +12,7 @@ import (
1212 "encoding/json"
1313 "fmt"
1414 "iter"
15- "log "
15+ "maps "
1616 "net/url"
1717 "path/filepath"
1818 "slices"
@@ -43,6 +43,7 @@ type Server struct {
4343 sessions []* ServerSession
4444 sendingMethodHandler_ MethodHandler [* ServerSession ]
4545 receivingMethodHandler_ MethodHandler [* ServerSession ]
46+ resourceSubscriptions map [string ]map [* ServerSession ]bool // uri -> session -> bool
4647}
4748
4849// ServerOptions is used to configure behavior of the server.
@@ -64,6 +65,10 @@ type ServerOptions struct {
6465 // If the peer fails to respond to pings originating from the keepalive check,
6566 // the session is automatically closed.
6667 KeepAlive time.Duration
68+ // Function called when a client session subscribes to a resource.
69+ SubscribeHandler func (context.Context , * SubscribeParams ) error
70+ // Function called when a client session unsubscribes from a resource.
71+ UnsubscribeHandler func (context.Context , * UnsubscribeParams ) error
6772}
6873
6974// NewServer creates a new MCP server. The resulting server has no features:
@@ -89,7 +94,12 @@ func NewServer(impl *Implementation, opts *ServerOptions) *Server {
8994 if opts .PageSize == 0 {
9095 opts .PageSize = DefaultPageSize
9196 }
92-
97+ if opts .SubscribeHandler != nil && opts .UnsubscribeHandler == nil {
98+ panic ("SubscribeHandler requires UnsubscribeHandler" )
99+ }
100+ if opts .UnsubscribeHandler != nil && opts .SubscribeHandler == nil {
101+ panic ("UnsubscribeHandler requires SubscribeHandler" )
102+ }
93103 return & Server {
94104 impl : impl ,
95105 opts : * opts ,
@@ -99,6 +109,7 @@ func NewServer(impl *Implementation, opts *ServerOptions) *Server {
99109 resourceTemplates : newFeatureSet (func (t * serverResourceTemplate ) string { return t .resourceTemplate .URITemplate }),
100110 sendingMethodHandler_ : defaultSendingMethodHandler [* ServerSession ],
101111 receivingMethodHandler_ : defaultReceivingMethodHandler [* ServerSession ],
112+ resourceSubscriptions : make (map [string ]map [* ServerSession ]bool ),
102113 }
103114}
104115
@@ -120,13 +131,18 @@ func (s *Server) RemovePrompts(names ...string) {
120131}
121132
122133// AddTool adds a [Tool] to the server, or replaces one with the same name.
123- // The tool's input schema must be non-nil.
124134// The Tool argument must not be modified after this call.
135+ //
136+ // The tool's input schema must be non-nil. For a tool that takes no input,
137+ // or one where any input is valid, set [Tool.InputSchema] to the empty schema,
138+ // &jsonschema.Schema{}.
125139func (s * Server ) AddTool (t * Tool , h ToolHandler ) {
126- // TODO(jba): This is a breaking behavior change. Add before v0.2.0?
127140 if t .InputSchema == nil {
128- log .Printf ("mcp: tool %q has a nil input schema. This will panic in a future release." , t .Name )
129- // panic(fmt.Sprintf("adding tool %q: nil input schema", t.Name))
141+ // This prevents the tool author from forgetting to write a schema where
142+ // one should be provided. If we papered over this by supplying the empty
143+ // schema, then every input would be validated and the problem wouldn't be
144+ // discovered until runtime, when the LLM sent bad data.
145+ panic (fmt .Sprintf ("adding tool %q: nil input schema" , t .Name ))
130146 }
131147 if err := addToolErr (s , t , h ); err != nil {
132148 panic (err )
@@ -225,6 +241,9 @@ func (s *Server) capabilities() *serverCapabilities {
225241 }
226242 if s .resources .len () > 0 || s .resourceTemplates .len () > 0 {
227243 caps .Resources = & resourceCapabilities {ListChanged : true }
244+ if s .opts .SubscribeHandler != nil {
245+ caps .Resources .Subscribe = true
246+ }
228247 }
229248 return caps
230249}
@@ -428,6 +447,57 @@ func fileResourceHandler(dir string) ResourceHandler {
428447 }
429448}
430449
450+ // ResourceUpdated sends a notification to all clients that have subscribed to the
451+ // resource specified in params. This method is the primary way for a
452+ // server author to signal that a resource has changed.
453+ func (s * Server ) ResourceUpdated (ctx context.Context , params * ResourceUpdatedNotificationParams ) error {
454+ s .mu .Lock ()
455+ subscribedSessions := s .resourceSubscriptions [params .URI ]
456+ sessions := slices .Collect (maps .Keys (subscribedSessions ))
457+ s .mu .Unlock ()
458+ notifySessions (sessions , notificationResourceUpdated , params )
459+ return nil
460+ }
461+
462+ func (s * Server ) subscribe (ctx context.Context , ss * ServerSession , params * SubscribeParams ) (* emptyResult , error ) {
463+ if s .opts .SubscribeHandler == nil {
464+ return nil , fmt .Errorf ("%w: server does not support resource subscriptions" , jsonrpc2 .ErrMethodNotFound )
465+ }
466+ if err := s .opts .SubscribeHandler (ctx , params ); err != nil {
467+ return nil , err
468+ }
469+
470+ s .mu .Lock ()
471+ defer s .mu .Unlock ()
472+ if s .resourceSubscriptions [params .URI ] == nil {
473+ s .resourceSubscriptions [params .URI ] = make (map [* ServerSession ]bool )
474+ }
475+ s.resourceSubscriptions [params.URI ][ss ] = true
476+
477+ return & emptyResult {}, nil
478+ }
479+
480+ func (s * Server ) unsubscribe (ctx context.Context , ss * ServerSession , params * UnsubscribeParams ) (* emptyResult , error ) {
481+ if s .opts .UnsubscribeHandler == nil {
482+ return nil , jsonrpc2 .ErrMethodNotFound
483+ }
484+
485+ if err := s .opts .UnsubscribeHandler (ctx , params ); err != nil {
486+ return nil , err
487+ }
488+
489+ s .mu .Lock ()
490+ defer s .mu .Unlock ()
491+ if subscribedSessions , ok := s .resourceSubscriptions [params .URI ]; ok {
492+ delete (subscribedSessions , ss )
493+ if len (subscribedSessions ) == 0 {
494+ delete (s .resourceSubscriptions , params .URI )
495+ }
496+ }
497+
498+ return & emptyResult {}, nil
499+ }
500+
431501// Run runs the server over the given transport, which must be persistent.
432502//
433503// Run blocks until the client terminates the connection or the provided
@@ -475,6 +545,10 @@ func (s *Server) disconnect(cc *ServerSession) {
475545 s .sessions = slices .DeleteFunc (s .sessions , func (cc2 * ServerSession ) bool {
476546 return cc2 == cc
477547 })
548+
549+ for _ , subscribedSessions := range s .resourceSubscriptions {
550+ delete (subscribedSessions , cc )
551+ }
478552}
479553
480554// Connect connects the MCP server over the given transport and starts handling
@@ -540,7 +614,7 @@ func (ss *ServerSession) ID() string {
540614
541615// Ping pings the client.
542616func (ss * ServerSession ) Ping (ctx context.Context , params * PingParams ) error {
543- _ , err := handleSend [* emptyResult ](ctx , ss , methodPing , params )
617+ _ , err := handleSend [* emptyResult ](ctx , ss , methodPing , orZero [ Params ]( params ) )
544618 return err
545619}
546620
@@ -616,6 +690,8 @@ var serverMethodInfos = map[string]methodInfo{
616690 methodListResourceTemplates : newMethodInfo (serverMethod ((* Server ).listResourceTemplates )),
617691 methodReadResource : newMethodInfo (serverMethod ((* Server ).readResource )),
618692 methodSetLevel : newMethodInfo (sessionMethod ((* ServerSession ).setLevel )),
693+ methodSubscribe : newMethodInfo (serverMethod ((* Server ).subscribe )),
694+ methodUnsubscribe : newMethodInfo (serverMethod ((* Server ).unsubscribe )),
619695 notificationInitialized : newMethodInfo (serverMethod ((* Server ).callInitializedHandler )),
620696 notificationRootsListChanged : newMethodInfo (serverMethod ((* Server ).callRootsListChangedHandler )),
621697 notificationProgress : newMethodInfo (sessionMethod ((* ServerSession ).callProgressNotificationHandler )),
0 commit comments