mcp: support url mode elicitation (#646)

Add initial support for URL mode elicitation, by introducing the new
types, as well as both server and client-side validation.

A follow-up CL will add support for ErrURLElicitationRequired.

For #623
This commit is contained in:
Robert Findley 2025-11-23 12:58:28 -05:00 • committed by GitHub
commit 21fb03dd1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 398 additions and 60 deletions

View file

@ -16,3 +16,10 @@ v2.
landing after the SDK was at v1, we missed an opportunity to panic on invalid
tool names. Instead, we have to simply produce an error log. In v2, we should
panic.
- Inconsistent naming.
- `ResourceUpdatedNotificationsParams` should probably have just been
`ResourceUpdatedParams`, as we don't include the word 'notification' in
other notification param types.
- Similarly, `ProgressNotificationParams` should probably have been
`ProgressParams`.

View file

@ -7,6 +7,7 @@ package main
import (
"context"
_ "embed"
"encoding/base64"
"flag"
"fmt"
@ -17,6 +18,7 @@ import (
"os"
"runtime"
"strings"
"sync/atomic"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
@ -51,12 +53,8 @@ func main() {
CompletionHandler: complete, // support completions by setting this handler
}
// Optionally add an icon to the server implementation.
icons, err := iconToBase64DataURL("./mcp.png")
if err != nil {
log.Fatalf("failed to read icon: %v", err)
}
// Add an icon to the server implementation.
icons := mcpIcons()
server := mcp.NewServer(&mcp.Implementation{Name: "everything", WebsiteURL: "https://example.com", Icons: icons}, opts)
// Add tools that exercise different features of the protocol.
@ -67,7 +65,8 @@ func main() {
mcp.AddTool(server, &mcp.Tool{Name: "ping"}, pingingTool) // performs a ping
mcp.AddTool(server, &mcp.Tool{Name: "log"}, loggingTool) // performs a log
mcp.AddTool(server, &mcp.Tool{Name: "sample"}, samplingTool) // performs sampling
mcp.AddTool(server, &mcp.Tool{Name: "elicit"}, elicitingTool) // performs elicitation
mcp.AddTool(server, &mcp.Tool{Name: "elicit (form)"}, elicitFormTool) // performs form elicitation
mcp.AddTool(server, &mcp.Tool{Name: "elicit (url)"}, elicitURLTool) // performs url elicitation
mcp.AddTool(server, &mcp.Tool{Name: "roots"}, rootsTool) // lists roots
// Add a basic prompt.
@ -235,7 +234,7 @@ func samplingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.Ca
}, nil, nil
}
func elicitingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
func elicitFormTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
res, err := req.Session.Elicit(ctx, &mcp.ElicitParams{
Message: "provide a random string",
RequestedSchema: &jsonschema.Schema{
@ -255,6 +254,26 @@ func elicitingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.C
}, nil, nil
}
var elicitations atomic.Int32
func elicitURLTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
elicitID := fmt.Sprintf("%d", elicitations.Add(1))
_, err := req.Session.Elicit(ctx, &mcp.ElicitParams{
Message: "submit a string",
URL: fmt.Sprintf("http://localhost:6062?id=%s", elicitID),
ElicitationID: elicitID,
})
if err != nil {
return nil, nil, fmt.Errorf("eliciting failed: %v", err)
}
// TODO: actually wait for the elicitation form to be submitted.
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "(elicitation pending)"},
},
}, nil, nil
}
func complete(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
return &mcp.CompleteResult{
Completion: mcp.CompletionResultDetails{
@ -264,15 +283,14 @@ func complete(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResul
}, nil
}
func iconToBase64DataURL(path string) ([]mcp.Icon, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
//go:embed mcp.png
var mcpIconData []byte
func mcpIcons() []mcp.Icon {
return []mcp.Icon{{
Source: "data:image/png;base64," + base64.StdEncoding.EncodeToString(data),
Source: "data:image/png;base64," + base64.StdEncoding.EncodeToString(mcpIconData),
MIMEType: "image/png",
Sizes: []string{"48x48"},
Theme: "light", // or "dark" or empty
}}, nil
}}
}

View file

@ -15,3 +15,10 @@ v2.
landing after the SDK was at v1, we missed an opportunity to panic on invalid
tool names. Instead, we have to simply produce an error log. In v2, we should
panic.
- Inconsistent naming.
- `ResourceUpdatedNotificationsParams` should probably have just been
`ResourceUpdatedParams`, as we don't include the word 'notification' in
other notification param types.
- Similarly, `ProgressNotificationParams` should probably have been
`ProgressParams`.

View file

@ -66,6 +66,11 @@ type ClientOptions struct {
// Setting ElicitationHandler to a non-nil value causes the client to
// advertise the elicitation capability.
ElicitationHandler func(context.Context, *ElicitRequest) (*ElicitResult, error)
// ElicitationModes specifies the elicitation modes supported by the client.
// If ElicitationHandler is set and ElicitationModes is empty, it defaults to ["form"].
ElicitationModes []string
// ElicitationCompleteHandler handles incoming notifications for notifications/elicitation/complete.
ElicitationCompleteHandler func(context.Context, *ElicitationCompleteNotificationRequest)
// Handlers for notifications from the server.
ToolListChangedHandler func(context.Context, *ToolListChangedRequest)
PromptListChangedHandler func(context.Context, *PromptListChangedRequest)
@ -123,6 +128,15 @@ func (c *Client) capabilities() *ClientCapabilities {
}
if c.opts.ElicitationHandler != nil {
caps.Elicitation = &ElicitationCapabilities{}
modes := c.opts.ElicitationModes
if len(modes) == 0 || slices.Contains(modes, "form") {
// Technically, the empty ElicitationCapabilities value is equivalent to
// {"form":{}} for backward compatibility, but we explicitly set the form
// capability.
caps.Elicitation.Form = &FormElicitationCapabilities{}
} else if slices.Contains(modes, "url") {
caps.Elicitation.URL = &URLElicitationCapabilities{}
}
}
return caps
}
@ -297,40 +311,55 @@ func (c *Client) createMessage(ctx context.Context, req *CreateMessageRequest) (
func (c *Client) elicit(ctx context.Context, req *ElicitRequest) (*ElicitResult, error) {
if c.opts.ElicitationHandler == nil {
// TODO: wrap or annotate this error? Pick a standard code?
return nil, jsonrpc2.NewError(codeUnsupportedMethod, "client does not support elicitation")
return nil, jsonrpc2.NewError(codeInvalidParams, "client does not support elicitation")
}
// Validate that the requested schema only contains top-level properties without nesting
schema, err := validateElicitSchema(req.Params.RequestedSchema)
if err != nil {
return nil, jsonrpc2.NewError(codeInvalidParams, err.Error())
// Validate the elicitation parameters based on the mode.
mode := req.Params.Mode
if mode == "" {
mode = "form"
}
res, err := c.opts.ElicitationHandler(ctx, req)
if err != nil {
return nil, err
}
// Validate elicitation result content against requested schema
if schema != nil && res.Content != nil {
// TODO: is this the correct behavior if validation fails?
// It isn't the *server's* params that are invalid, so why would we return
// this code to the server?
resolved, err := schema.Resolve(nil)
switch mode {
case "form":
if req.Params.URL != "" {
return nil, jsonrpc2.NewError(codeInvalidParams, "URL must not be set for form elicitation")
}
schema, err := validateElicitSchema(req.Params.RequestedSchema)
if err != nil {
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("failed to resolve requested schema: %v", err))
return nil, jsonrpc2.NewError(codeInvalidParams, err.Error())
}
if err := resolved.Validate(res.Content); err != nil {
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("elicitation result content does not match requested schema: %v", err))
}
err = resolved.ApplyDefaults(&res.Content)
res, err := c.opts.ElicitationHandler(ctx, req)
if err != nil {
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("failed to apply schema defalts to elicitation result: %v", err))
return nil, err
}
// Validate elicitation result content against requested schema.
if schema != nil && res.Content != nil {
resolved, err := schema.Resolve(nil)
if err != nil {
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("failed to resolve requested schema: %v", err))
}
if err := resolved.Validate(res.Content); err != nil {
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("elicitation result content does not match requested schema: %v", err))
}
err = resolved.ApplyDefaults(&res.Content)
if err != nil {
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("failed to apply schema defalts to elicitation result: %v", err))
}
}
return res, nil
case "url":
if req.Params.RequestedSchema != nil {
return nil, jsonrpc2.NewError(codeInvalidParams, "requestedSchema must not be set for URL elicitation")
}
if req.Params.URL == "" {
return nil, jsonrpc2.NewError(codeInvalidParams, "URL must be set for URL elicitation")
}
// No schema validation for URL mode, just pass through to handler.
return c.opts.ElicitationHandler(ctx, req)
default:
return nil, jsonrpc2.NewError(codeInvalidParams, fmt.Sprintf("unsupported elicitation mode: %q", mode))
}
return res, nil
}
// validateElicitSchema validates that the schema conforms to MCP elicitation schema requirements.
@ -528,6 +557,7 @@ var clientMethodInfos = map[string]methodInfo{
notificationResourceUpdated: newClientMethodInfo(clientMethod((*Client).callResourceUpdatedHandler), notification|missingParamsOK),
notificationLoggingMessage: newClientMethodInfo(clientMethod((*Client).callLoggingHandler), notification),
notificationProgress: newClientMethodInfo(clientSessionMethod((*ClientSession).callProgressNotificationHandler), notification),
notificationElicitationComplete: newClientMethodInfo(clientMethod((*Client).callElicitationCompleteHandler), notification|missingParamsOK),
}
func (cs *ClientSession) sendingMethodInfos() map[string]methodInfo {
@ -692,6 +722,13 @@ func (cs *ClientSession) callProgressNotificationHandler(ctx context.Context, pa
return nil, nil
}
func (c *Client) callElicitationCompleteHandler(ctx context.Context, req *ElicitationCompleteNotificationRequest) (Result, error) {
if h := c.opts.ElicitationCompleteHandler; h != nil {
h(ctx, req)
}
return nil, nil
}
// NotifyProgress sends a progress notification from the client to the server
// associated with this session.
// This can be used if the client is performing a long-running task that was

190
mcp/elicitation_test.go Normal file
View file

@ -0,0 +1,190 @@
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
// Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package mcp
import (
"context"
"errors"
"strings"
"testing"
"time"
"github.com/google/jsonschema-go/jsonschema"
)
// TODO: migrate other elicitation tests here.
func TestElicitationURLMode(t *testing.T) {
ctx := context.Background()
clientErr := errors.New("client failed to elicit")
testCases := []struct {
name string
handler func(context.Context, *ElicitRequest) (*ElicitResult, error)
params *ElicitParams
wantResultAction string
wantErrMsg string
wantErrCode int64
}{
{
name: "success",
handler: func(ctx context.Context, req *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "accept"}, nil
},
params: &ElicitParams{
Mode: "url",
Message: "Please provide information via URL",
URL: "https://example.com/form",
},
wantResultAction: "accept",
},
{
name: "decline",
handler: func(ctx context.Context, req *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "decline"}, nil
},
params: &ElicitParams{
Mode: "url",
Message: "Please provide information via URL",
URL: "https://example.com/form",
},
wantResultAction: "decline",
},
{
name: "client error",
handler: func(ctx context.Context, req *ElicitRequest) (*ElicitResult, error) {
return nil, clientErr
},
params: &ElicitParams{
Mode: "url",
Message: "This should fail",
URL: "https://example.com/form",
},
wantErrMsg: clientErr.Error(),
},
{
name: "missing url",
handler: func(ctx context.Context, req *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "accept"}, nil
},
params: &ElicitParams{
Mode: "url",
Message: "URL is missing",
},
wantErrMsg: "URL must be set for URL elicitation",
wantErrCode: codeInvalidParams,
},
{
name: "schema not allowed",
handler: func(ctx context.Context, req *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "accept"}, nil
},
params: &ElicitParams{
Mode: "url",
Message: "Schema is not allowed",
URL: "https://example.com/form",
RequestedSchema: &jsonschema.Schema{
Type: "object",
},
},
wantErrMsg: "requestedSchema must not be set for URL elicitation",
wantErrCode: codeInvalidParams,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ct, st := NewInMemoryTransports()
s := NewServer(testImpl, nil)
ss, err := s.Connect(ctx, st, nil)
if err != nil {
t.Fatal(err)
}
defer ss.Close()
c := NewClient(testImpl, &ClientOptions{
ElicitationModes: []string{"url"},
ElicitationHandler: tc.handler,
})
cs, err := c.Connect(ctx, ct, nil)
if err != nil {
t.Fatal(err)
}
defer cs.Close()
result, err := ss.Elicit(ctx, tc.params)
if tc.wantErrMsg != "" {
if err == nil || !strings.Contains(err.Error(), tc.wantErrMsg) {
t.Fatalf("Elicit(...): got error %v, want containing %q", err, tc.wantErrMsg)
}
if tc.wantErrCode != 0 {
if code := errorCode(err); code != tc.wantErrCode {
t.Errorf("Elicit(...): got error code %d, want %d", code, tc.wantErrCode)
}
}
} else {
if err != nil {
t.Fatalf("Elicit failed: %v", err)
}
if result.Action != tc.wantResultAction {
t.Errorf("Elicit(...): got action %q, want %q", result.Action, tc.wantResultAction)
}
}
})
}
}
func TestElicitationCompleteNotification(t *testing.T) {
ctx := context.Background()
var elicitationCompleteCh = make(chan *ElicitationCompleteParams, 1)
c := NewClient(testImpl, &ClientOptions{
ElicitationModes: []string{"url"},
ElicitationHandler: func(context.Context, *ElicitRequest) (*ElicitResult, error) {
return &ElicitResult{Action: "accept"}, nil
},
ElicitationCompleteHandler: func(_ context.Context, req *ElicitationCompleteNotificationRequest) {
elicitationCompleteCh <- req.Params
},
})
cs, ss, cleanup := basicClientServerConnection(t, c, nil, nil)
_ = cs // Dummy usage to avoid "declared and not used" error.
defer cleanup()
// 1. Server initiates a URL elicitation
elicitID := "testElicitationID-123"
resp, err := ss.Elicit(ctx, &ElicitParams{
Mode: "url",
Message: "Please complete this form: ",
URL: "https://example.com/form?id=" + elicitID,
ElicitationID: elicitID,
})
if err != nil {
t.Fatalf("Elicit failed: %v", err)
}
if resp.Action != "accept" {
t.Fatalf("Elicit action is %q, want %q", resp.Action, "accept")
}
// 2. Server sends elicitation complete notification (simulating out-of-band completion)
err = handleNotify(ctx, notificationElicitationComplete, newServerRequest(ss, &ElicitationCompleteParams{
ElicitationID: elicitID,
}))
if err != nil {
t.Fatalf("failed to send elicitation complete notification: %v", err)
}
// 3. Client should receive the notification
select {
case gotParams := <-elicitationCompleteCh:
if gotParams.ElicitationID != elicitID {
t.Errorf("elicitationComplete notification ID mismatch: got %q, want %q", gotParams.ElicitationID, elicitID)
}
case <-time.After(5 * time.Second):
t.Fatal("timed out waiting for elicitation complete notification")
}
}

View file

@ -62,9 +62,10 @@ func TestEndToEnd(t *testing.T) {
// Channels to check if notification callbacks happened.
notificationChans := map[string]chan int{}
for _, name := range []string{"initialized", "roots", "tools", "prompts", "resources", "progress_server", "progress_client", "resource_updated", "subscribe", "unsubscribe"} {
for _, name := range []string{"initialized", "roots", "tools", "prompts", "resources", "progress_server", "progress_client", "resource_updated", "subscribe", "unsubscribe", "elicitation_complete"} {
notificationChans[name] = make(chan int, 1)
}
waitForNotification := func(t *testing.T, name string) {
t.Helper()
select {
@ -150,6 +151,9 @@ func TestEndToEnd(t *testing.T) {
ResourceUpdatedHandler: func(context.Context, *ResourceUpdatedNotificationRequest) {
notificationChans["resource_updated"] <- 0
},
ElicitationCompleteHandler: func(_ context.Context, req *ElicitationCompleteNotificationRequest) {
notificationChans["elicitation_complete"] <- 0
},
}
c := NewClient(testImpl, opts)
rootAbs, err := filepath.Abs(filepath.FromSlash("testdata/files"))
@ -985,8 +989,8 @@ func TestElicitationUnsupportedMethod(t *testing.T) {
if err == nil {
t.Error("expected error when ElicitationHandler is not provided, got nil")
}
if code := errorCode(err); code != codeUnsupportedMethod {
t.Errorf("got error code %d, want %d (CodeUnsupportedMethod)", code, codeUnsupportedMethod)
if code := errorCode(err); code != -1 {
t.Errorf("got error code %d, want -1", code)
}
if !strings.Contains(err.Error(), "does not support elicitation") {
t.Errorf("error should mention unsupported elicitation, got: %v", err)
@ -1414,7 +1418,7 @@ func TestElicitationProgressToken(t *testing.T) {
func TestElicitationCapabilityDeclaration(t *testing.T) {
ctx := context.Background()
t.Run("with_handler", func(t *testing.T) {
t.Run("with handler", func(t *testing.T) {
ct, st := NewInMemoryTransports()
// Client with ElicitationHandler should declare capability
@ -1451,7 +1455,7 @@ func TestElicitationCapabilityDeclaration(t *testing.T) {
}
})
t.Run("without_handler", func(t *testing.T) {
t.Run("without handler", func(t *testing.T) {
ct, st := NewInMemoryTransports()
// Client without ElicitationHandler should not declare capability
@ -1483,8 +1487,8 @@ func TestElicitationCapabilityDeclaration(t *testing.T) {
if err == nil {
t.Error("expected UnsupportedMethod error when no capability declared")
}
if code := errorCode(err); code != codeUnsupportedMethod {
t.Errorf("got error code %d, want %d (CodeUnsupportedMethod)", code, codeUnsupportedMethod)
if code := errorCode(err); code != -1 {
t.Errorf("got error code %d, want -1", code)
}
})
}

View file

@ -879,7 +879,20 @@ func (x *RootsListChangedParams) SetProgressToken(t any) { setProgressToken(x, t
type SamplingCapabilities struct{}
// ElicitationCapabilities describes the capabilities for elicitation.
type ElicitationCapabilities struct{}
//
// If neither Form nor URL is set, the 'Form' capabilitiy is assumed.
type ElicitationCapabilities struct {
Form *FormElicitationCapabilities
URL *URLElicitationCapabilities
}
// FormElicitationCapabilities describes capabilities for form elicitation.
type FormElicitationCapabilities struct {
}
// URLElicitationCapabilities describes capabilities for url elicitation.
type URLElicitationCapabilities struct {
}
// Describes a message issued to or received from an LLM API.
type SamplingMessage struct {
@ -1067,6 +1080,10 @@ type ElicitParams struct {
// This property is reserved by the protocol to allow clients and servers to
// attach additional metadata to their responses.
Meta `json:"_meta,omitempty"`
// The mode of elicitation to use.
//
// If unset, will be inferred from the other fields.
Mode string `json:"mode"`
// The message to present to the user.
Message string `json:"message"`
// A JSON schema object defining the requested elicitation schema.
@ -1080,7 +1097,17 @@ type ElicitParams struct {
// map[string]any).
//
// Only top-level properties are allowed, without nesting.
RequestedSchema any `json:"requestedSchema"`
//
// This is only used for "form" elicitation.
RequestedSchema any `json:"requestedSchema,omitempty"`
// The URL to present to the user.
//
// This is only used for "url" elicitation.
URL string `json:"url,omitempty"`
// The ID of the elicitation.
//
// This is only used for "url" elicitation.
ElicitationID string `json:"elicitationId,omitempty"`
}
func (x *ElicitParams) isParams() {}
@ -1105,6 +1132,18 @@ type ElicitResult struct {
func (*ElicitResult) isResult() {}
// ElicitationCompleteParams is sent from the server to the client, informing it that an out-of-band elicitation interaction has completed.
type ElicitationCompleteParams struct {
// This property is reserved by the protocol to allow clients and servers to
// attach additional metadata to their responses.
Meta `json:"_meta,omitempty"`
// The ID of the elicitation that has completed. This must correspond to the
// elicitationId from the original elicitation/create request.
ElicitationID string `json:"elicitationId"`
}
func (*ElicitationCompleteParams) isParams() {}
// An Implementation describes the name and version of an MCP implementation, with an optional
// title for UI representation.
type Implementation struct {
@ -1171,6 +1210,7 @@ const (
methodComplete = "completion/complete"
methodCreateMessage = "sampling/createMessage"
methodElicit = "elicitation/create"
notificationElicitationComplete = "notifications/elicitation/complete"
methodGetPrompt = "prompts/get"
methodInitialize = "initialize"
notificationInitialized = "notifications/initialized"

View file

@ -23,15 +23,16 @@ type (
)
type (
CreateMessageRequest = ClientRequest[*CreateMessageParams]
ElicitRequest = ClientRequest[*ElicitParams]
initializedClientRequest = ClientRequest[*InitializedParams]
InitializeRequest = ClientRequest[*InitializeParams]
ListRootsRequest = ClientRequest[*ListRootsParams]
LoggingMessageRequest = ClientRequest[*LoggingMessageParams]
ProgressNotificationClientRequest = ClientRequest[*ProgressNotificationParams]
PromptListChangedRequest = ClientRequest[*PromptListChangedParams]
ResourceListChangedRequest = ClientRequest[*ResourceListChangedParams]
ResourceUpdatedNotificationRequest = ClientRequest[*ResourceUpdatedNotificationParams]
ToolListChangedRequest = ClientRequest[*ToolListChangedParams]
CreateMessageRequest = ClientRequest[*CreateMessageParams]
ElicitRequest = ClientRequest[*ElicitParams]
initializedClientRequest = ClientRequest[*InitializedParams]
InitializeRequest = ClientRequest[*InitializeParams]
ListRootsRequest = ClientRequest[*ListRootsParams]
LoggingMessageRequest = ClientRequest[*LoggingMessageParams]
ProgressNotificationClientRequest = ClientRequest[*ProgressNotificationParams]
PromptListChangedRequest = ClientRequest[*PromptListChangedParams]
ResourceListChangedRequest = ClientRequest[*ResourceListChangedParams]
ResourceUpdatedNotificationRequest = ClientRequest[*ResourceUpdatedNotificationParams]
ToolListChangedRequest = ClientRequest[*ToolListChangedParams]
ElicitationCompleteNotificationRequest = ClientRequest[*ElicitationCompleteParams]
)

View file

@ -10,6 +10,7 @@ import (
"encoding/base64"
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"iter"
"log/slog"
@ -1018,6 +1019,36 @@ func (ss *ServerSession) Elicit(ctx context.Context, params *ElicitParams) (*Eli
if err := ss.checkInitialized(methodElicit); err != nil {
return nil, err
}
if params == nil {
return nil, fmt.Errorf("%w: params cannot be nil", jsonrpc2.ErrInvalidParams)
}
if params.Mode == "" {
params2 := *params
if params.URL != "" || params.ElicitationID != "" {
params2.Mode = "url"
} else {
params2.Mode = "form"
}
params = &params2
}
if iparams := ss.InitializeParams(); iparams == nil || iparams.Capabilities == nil || iparams.Capabilities.Elicitation == nil {
return nil, fmt.Errorf("client does not support elicitation")
}
caps := ss.InitializeParams().Capabilities.Elicitation
switch params.Mode {
case "form":
if caps.Form == nil && caps.URL != nil {
// Note: if both 'Form' and 'URL' are nil, we assume the client supports
// form elicitation for backward compatibility.
return nil, errors.New(`client does not support "form" elicitation`)
}
case "url":
if caps.URL == nil {
return nil, errors.New(`client does not support "url" elicitation`)
}
}
res, err := handleSend[*ElicitResult](ctx, methodElicit, newServerRequest(ss, orZero[Params](params)))
if err != nil {

View file

@ -335,6 +335,9 @@ func clientSessionMethod[P Params, R Result](f func(*ClientSession, context.Cont
const (
codeResourceNotFound = -32002
// The error code if the method exists and was called properly, but the peer does not support it.
//
// TODO(rfindley): this code is wrong, and we should fix it to be
// consistent with other SDKs.
codeUnsupportedMethod = -31001
// The error code for invalid parameters
codeInvalidParams = -32602