@@ -6,12 +6,14 @@ package mcp
66
77import (
88 "context"
9+ "encoding/json"
910 "fmt"
1011 "iter"
1112 "slices"
1213 "sync"
1314 "time"
1415
16+ "github.com/google/jsonschema-go/jsonschema"
1517 "github.com/modelcontextprotocol/go-sdk/internal/jsonrpc2"
1618 "github.com/modelcontextprotocol/go-sdk/jsonrpc"
1719)
@@ -56,6 +58,9 @@ type ClientOptions struct {
5658 // Handler for sampling.
5759 // Called when a server calls CreateMessage.
5860 CreateMessageHandler func (context.Context , * CreateMessageRequest ) (* CreateMessageResult , error )
61+ // Handler for elicitation.
62+ // Called when a server requests user input via Elicit.
63+ ElicitationHandler func (context.Context , * ElicitRequest ) (* ElicitResult , error )
5964 // Handlers for notifications from the server.
6065 ToolListChangedHandler func (context.Context , * ToolListChangedRequest )
6166 PromptListChangedHandler func (context.Context , * PromptListChangedRequest )
@@ -111,6 +116,9 @@ func (c *Client) capabilities() *ClientCapabilities {
111116 if c .opts .CreateMessageHandler != nil {
112117 caps .Sampling = & SamplingCapabilities {}
113118 }
119+ if c .opts .ElicitationHandler != nil {
120+ caps .Elicitation = & ElicitationCapabilities {}
121+ }
114122 return caps
115123}
116124
@@ -268,6 +276,168 @@ func (c *Client) createMessage(ctx context.Context, req *CreateMessageRequest) (
268276 return c .opts .CreateMessageHandler (ctx , req )
269277}
270278
279+ func (c * Client ) elicit (ctx context.Context , req * ElicitRequest ) (* ElicitResult , error ) {
280+ if c .opts .ElicitationHandler == nil {
281+ // TODO: wrap or annotate this error? Pick a standard code?
282+ return nil , jsonrpc2 .NewError (CodeUnsupportedMethod , "client does not support elicitation" )
283+ }
284+
285+ // Validate that the requested schema only contains top-level properties without nesting
286+ if err := validateElicitSchema (req .Params .RequestedSchema ); err != nil {
287+ return nil , jsonrpc2 .NewError (CodeInvalidParams , err .Error ())
288+ }
289+
290+ res , err := c .opts .ElicitationHandler (ctx , req )
291+ if err != nil {
292+ return nil , err
293+ }
294+
295+ // Validate elicitation result content against requested schema
296+ if req .Params .RequestedSchema != nil && res .Content != nil {
297+ resolved , err := req .Params .RequestedSchema .Resolve (nil )
298+ if err != nil {
299+ return nil , jsonrpc2 .NewError (CodeInvalidParams , fmt .Sprintf ("failed to resolve requested schema: %v" , err ))
300+ }
301+
302+ if err := resolved .Validate (res .Content ); err != nil {
303+ return nil , jsonrpc2 .NewError (CodeInvalidParams , fmt .Sprintf ("elicitation result content does not match requested schema: %v" , err ))
304+ }
305+ }
306+
307+ return res , nil
308+ }
309+
310+ // validateElicitSchema validates that the schema conforms to MCP elicitation schema requirements.
311+ // Per the MCP specification, elicitation schemas are limited to flat objects with primitive properties only.
312+ func validateElicitSchema (schema * jsonschema.Schema ) error {
313+ if schema == nil {
314+ return nil // nil schema is allowed
315+ }
316+
317+ // The root schema must be of type "object" if specified
318+ if schema .Type != "" && schema .Type != "object" {
319+ return fmt .Errorf ("elicit schema must be of type 'object', got %q" , schema .Type )
320+ }
321+
322+ // Check if the schema has properties
323+ if schema .Properties != nil {
324+ for propName , propSchema := range schema .Properties {
325+ if propSchema == nil {
326+ continue
327+ }
328+
329+ if err := validateElicitProperty (propName , propSchema ); err != nil {
330+ return err
331+ }
332+ }
333+ }
334+
335+ return nil
336+ }
337+
338+ // validateElicitProperty validates a single property in an elicitation schema.
339+ func validateElicitProperty (propName string , propSchema * jsonschema.Schema ) error {
340+ // Check if this property has nested properties (not allowed)
341+ if len (propSchema .Properties ) > 0 {
342+ return fmt .Errorf ("elicit schema property %q contains nested properties, only primitive properties are allowed" , propName )
343+ }
344+
345+ // Validate based on the property type - only primitives are supported
346+ switch propSchema .Type {
347+ case "string" :
348+ return validateElicitStringProperty (propName , propSchema )
349+ case "number" , "integer" :
350+ return validateElicitNumberProperty (propName , propSchema )
351+ case "boolean" :
352+ return validateElicitBooleanProperty (propName , propSchema )
353+ default :
354+ return fmt .Errorf ("elicit schema property %q has unsupported type %q, only string, number, integer, and boolean are allowed" , propName , propSchema .Type )
355+ }
356+ }
357+
358+ // validateElicitStringProperty validates string-type properties, including enums.
359+ func validateElicitStringProperty (propName string , propSchema * jsonschema.Schema ) error {
360+ // Handle enum validation (enums are a special case of strings)
361+ if len (propSchema .Enum ) > 0 {
362+ // Enums must be string type (or untyped which defaults to string)
363+ if propSchema .Type != "" && propSchema .Type != "string" {
364+ return fmt .Errorf ("elicit schema property %q has enum values but type is %q, enums are only supported for string type" , propName , propSchema .Type )
365+ }
366+ // Enum values themselves are validated by the JSON schema library
367+ // Validate enumNames if present - must match enum length
368+ if propSchema .Extra != nil {
369+ if enumNamesRaw , exists := propSchema .Extra ["enumNames" ]; exists {
370+ // Type check enumNames - should be a slice
371+ if enumNamesSlice , ok := enumNamesRaw .([]interface {}); ok {
372+ if len (enumNamesSlice ) != len (propSchema .Enum ) {
373+ return fmt .Errorf ("elicit schema property %q has %d enum values but %d enumNames, they must match" , propName , len (propSchema .Enum ), len (enumNamesSlice ))
374+ }
375+ } else {
376+ return fmt .Errorf ("elicit schema property %q has invalid enumNames type, must be an array" , propName )
377+ }
378+ }
379+ }
380+ return nil
381+ }
382+
383+ // Validate format if specified - only specific formats are allowed
384+ if propSchema .Format != "" {
385+ allowedFormats := map [string ]bool {
386+ "email" : true ,
387+ "uri" : true ,
388+ "date" : true ,
389+ "date-time" : true ,
390+ }
391+ if ! allowedFormats [propSchema .Format ] {
392+ return fmt .Errorf ("elicit schema property %q has unsupported format %q, only email, uri, date, and date-time are allowed" , propName , propSchema .Format )
393+ }
394+ }
395+
396+ // Validate minLength constraint if specified
397+ if propSchema .MinLength != nil {
398+ if * propSchema .MinLength < 0 {
399+ return fmt .Errorf ("elicit schema property %q has invalid minLength %d, must be non-negative" , propName , * propSchema .MinLength )
400+ }
401+ }
402+
403+ // Validate maxLength constraint if specified
404+ if propSchema .MaxLength != nil {
405+ if * propSchema .MaxLength < 0 {
406+ return fmt .Errorf ("elicit schema property %q has invalid maxLength %d, must be non-negative" , propName , * propSchema .MaxLength )
407+ }
408+ // Check that maxLength >= minLength if both are specified
409+ if propSchema .MinLength != nil && * propSchema .MaxLength < * propSchema .MinLength {
410+ return fmt .Errorf ("elicit schema property %q has maxLength %d less than minLength %d" , propName , * propSchema .MaxLength , * propSchema .MinLength )
411+ }
412+ }
413+
414+ return nil
415+ }
416+
417+ // validateElicitNumberProperty validates number and integer-type properties.
418+ func validateElicitNumberProperty (propName string , propSchema * jsonschema.Schema ) error {
419+ if propSchema .Minimum != nil && propSchema .Maximum != nil {
420+ if * propSchema .Maximum < * propSchema .Minimum {
421+ return fmt .Errorf ("elicit schema property %q has maximum %g less than minimum %g" , propName , * propSchema .Maximum , * propSchema .Minimum )
422+ }
423+ }
424+
425+ return nil
426+ }
427+
428+ // validateElicitBooleanProperty validates boolean-type properties.
429+ func validateElicitBooleanProperty (propName string , propSchema * jsonschema.Schema ) error {
430+ // Validate default value if specified - must be a valid boolean
431+ if propSchema .Default != nil {
432+ var defaultValue bool
433+ if err := json .Unmarshal (propSchema .Default , & defaultValue ); err != nil {
434+ return fmt .Errorf ("elicit schema property %q has invalid default value, must be a boolean: %v" , propName , err )
435+ }
436+ }
437+
438+ return nil
439+ }
440+
271441// AddSendingMiddleware wraps the current sending method handler using the provided
272442// middleware. Middleware is applied from right to left, so that the first one is
273443// executed first.
@@ -308,6 +478,7 @@ var clientMethodInfos = map[string]methodInfo{
308478 methodPing : newClientMethodInfo (clientSessionMethod ((* ClientSession ).ping ), missingParamsOK ),
309479 methodListRoots : newClientMethodInfo (clientMethod ((* Client ).listRoots ), missingParamsOK ),
310480 methodCreateMessage : newClientMethodInfo (clientMethod ((* Client ).createMessage ), 0 ),
481+ methodElicit : newClientMethodInfo (clientMethod ((* Client ).elicit ), missingParamsOK ),
311482 notificationCancelled : newClientMethodInfo (clientSessionMethod ((* ClientSession ).cancel ), notification | missingParamsOK ),
312483 notificationToolListChanged : newClientMethodInfo (clientMethod ((* Client ).callToolChangedHandler ), notification | missingParamsOK ),
313484 notificationPromptListChanged : newClientMethodInfo (clientMethod ((* Client ).callPromptChangedHandler ), notification | missingParamsOK ),
0 commit comments