-
Notifications
You must be signed in to change notification settings - Fork 690
feat: implement MCP elicitation support (#413) #495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThis change introduces elicitation support to the MCP client and server. It adds new types and interfaces for elicitation requests and results, extends client and server capabilities to advertise elicitation support, implements request/response handling for elicitation in both stdio and in-process transports, and provides comprehensive tests and examples demonstrating elicitation workflows. Changes
Estimated code review effort4 (60–120 minutes) This PR introduces a new cross-cutting feature with protocol changes, new types, handler interfaces, updates to client/server/transport logic, and extensive testing and examples. Reviewing will require careful attention to protocol integration and concurrency in request/response handling. Possibly related PRs
Suggested labels
Suggested reviewers
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (13)
🚧 Files skipped from review as they are similar to previous changes (13)
✨ Finishing Touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
🧹 Nitpick comments (2)
server/session.go (1)
51-56
: Interface addition LGTM, but clarify blocking behaviour.
RequestElicitation
is (implicitly) expected to block until the client responds.
Please document this expectation in the interface comment (or make it explicit in the method name) so implementers know whether they must spawn a goroutine / select onctx.Done()
internally.-// RequestElicitation sends an elicitation request to the client and waits for response +// RequestElicitation sends an elicitation request to the client and MUST block +// until either a response is received or ctx is cancelled. +// Implementations should respect ctx cancellation to avoid server goroutine leaks.server/elicitation_test.go (1)
59-82
: Use consistent server configuration pattern.There's an inconsistency in how elicitation capability is enabled. Line 61 directly sets
server.capabilities.elicitation = mcp.ToBoolPtr(true)
, while other tests use theWithElicitation()
option (line 85). Consider using the option pattern consistently for better maintainability.func TestMCPServer_RequestElicitation_NoSession(t *testing.T) { - server := NewMCPServer("test", "1.0.0") - server.capabilities.elicitation = mcp.ToBoolPtr(true) + server := NewMCPServer("test", "1.0.0", WithElicitation())
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (18)
client/client.go
(5 hunks)client/elicitation.go
(1 hunks)client/elicitation_test.go
(1 hunks)client/inprocess_elicitation_test.go
(1 hunks)client/transport/inprocess.go
(3 hunks)client/transport/sse_test.go
(2 hunks)client/transport/streamable_http_test.go
(2 hunks)examples/elicitation/main.go
(1 hunks)mcp/tools.go
(3 hunks)mcp/types.go
(4 hunks)mcptest/mcptest.go
(1 hunks)server/elicitation.go
(1 hunks)server/elicitation_test.go
(1 hunks)server/inprocess_session.go
(3 hunks)server/server.go
(3 hunks)server/session.go
(1 hunks)server/stdio.go
(6 hunks)server/streamable_http_test.go
(1 hunks)
🧰 Additional context used
🧠 Learnings (14)
📓 Common learnings
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
mcptest/mcptest.go (5)
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: robert-jackson-glean
PR: mark3labs/mcp-go#214
File: server/sse.go:0-0
Timestamp: 2025-04-28T00:14:49.263Z
Learning: The SSE server in mcp-go implements path sanitization within the `WithDynamicBasePath` function that ensures the dynamic base path starts with "/" and has no trailing "/" to prevent double slashes in URL construction.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
server/server.go (3)
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
client/transport/sse_test.go (1)
Learnt from: leavez
PR: mark3labs/mcp-go#114
File: client/transport/sse.go:137-179
Timestamp: 2025-04-06T10:07:06.685Z
Learning: The SSE client implementation in the MCP-Go project uses a 30-second timeout for reading SSE events to match the behavior of the original implementation before the transport layer refactoring.
server/streamable_http_test.go (5)
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
server/elicitation.go (1)
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
mcp/tools.go (8)
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in mark3labs/mcp-go handles both InputSchema and RawInputSchema formats. When unmarshaling JSON, it first tries to parse into a structured ToolInputSchema format, and if that fails, it falls back to using the raw schema format, providing symmetry with the MarshalJSON method.
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
Learnt from: davidleitw
PR: mark3labs/mcp-go#451
File: mcp/tools.go:1192-1217
Timestamp: 2025-06-26T09:38:18.629Z
Learning: In mcp-go project, the maintainer prefers keeping builder pattern APIs simple without excessive validation for edge cases. The WithOutput* functions are designed to assume correct usage rather than defensive programming, following the principle of API simplicity over comprehensive validation.
Learnt from: lariel-fernandes
PR: mark3labs/mcp-go#428
File: www/docs/pages/servers/prompts.mdx:218-234
Timestamp: 2025-06-20T20:39:51.870Z
Learning: In the mcp-go library, the GetPromptParams.Arguments field is of type map[string]string, not map[string]interface{}, so direct string access without type assertions is safe and correct.
server/elicitation_test.go (2)
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
examples/elicitation/main.go (4)
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
client/inprocess_elicitation_test.go (2)
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
mcp/types.go (4)
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
Learnt from: floatingIce91
PR: mark3labs/mcp-go#401
File: server/server.go:1082-1092
Timestamp: 2025-06-23T11:10:42.948Z
Learning: In Go MCP server, ServerTool.Tool field is only used for tool listing and indexing, not for tool execution or middleware. During handleToolCall, only the Handler field is used, so dynamic tools don't need the Tool field populated.
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:0-0
Timestamp: 2025-03-04T07:00:57.111Z
Learning: The Tool struct in the mark3labs/mcp-go project should handle both InputSchema and RawInputSchema consistently between MarshalJSON and UnmarshalJSON methods, even though the tools response from MCP server typically doesn't contain rawInputSchema.
client/elicitation_test.go (1)
Learnt from: octo
PR: mark3labs/mcp-go#149
File: mcptest/mcptest.go:0-0
Timestamp: 2025-04-21T21:26:32.945Z
Learning: In the mcptest package, prefer returning errors from helper functions rather than calling t.Fatalf() directly, giving callers flexibility in how to handle errors.
client/client.go (1)
Learnt from: ezynda3
PR: mark3labs/mcp-go#461
File: server/sampling.go:22-26
Timestamp: 2025-06-30T07:13:17.052Z
Learning: In the mark3labs/mcp-go project, the MCPServer.capabilities field is a struct value (serverCapabilities), not a pointer, so it cannot be nil and doesn't require nil checking. Only pointer fields within the capabilities struct should be checked for nil.
server/stdio.go (1)
Learnt from: xinwo
PR: mark3labs/mcp-go#35
File: mcp/tools.go:107-137
Timestamp: 2025-03-04T06:59:43.882Z
Learning: Tool responses from the MCP server shouldn't contain RawInputSchema, which is why the UnmarshalJSON method for the Tool struct is implemented to handle only the structured InputSchema format.
🧬 Code Graph Analysis (9)
server/server.go (1)
mcp/utils.go (1)
ToBoolPtr
(817-819)
server/session.go (1)
mcp/types.go (2)
ElicitationRequest
(805-808)ElicitationResult
(819-826)
client/elicitation.go (1)
mcp/types.go (2)
ElicitationRequest
(805-808)ElicitationResult
(819-826)
server/elicitation.go (3)
server/server.go (1)
MCPServer
(138-163)mcp/types.go (2)
ElicitationRequest
(805-808)ElicitationResult
(819-826)server/session.go (2)
ClientSessionFromContext
(78-83)SessionWithElicitation
(52-56)
mcp/tools.go (1)
mcp/types.go (2)
Meta
(123-135)Content
(917-919)
examples/elicitation/main.go (5)
server/server.go (4)
MCPServer
(138-163)ToolHandlerFunc
(41-41)NewMCPServer
(300-326)WithElicitation
(286-290)mcp/tools.go (7)
CallToolRequest
(48-52)CallToolResult
(38-45)NewTool
(638-660)WithDescription
(681-685)WithString
(916-934)Required
(748-752)Description
(740-744)mcp/types.go (7)
ElicitationRequest
(805-808)Params
(167-167)ElicitationParams
(811-816)ElicitationResponseTypeAccept
(842-842)Content
(917-919)ElicitationResponseTypeDecline
(844-844)ElicitationResponseTypeCancel
(846-846)mcp/utils.go (1)
NewTextContent
(199-204)server/stdio.go (1)
NewStdioServer
(271-280)
server/inprocess_session.go (4)
mcp/types.go (3)
ElicitationRequest
(805-808)ElicitationResult
(819-826)JSONRPCNotification
(323-326)client/elicitation.go (1)
ElicitationHandler
(11-19)server/session.go (4)
ClientSession
(11-20)SessionWithLogging
(23-29)SessionWithClientInfo
(43-49)SessionWithElicitation
(52-56)server/sampling.go (1)
SessionWithSampling
(39-42)
client/client.go (4)
client/elicitation.go (1)
ElicitationHandler
(11-19)server/inprocess_session.go (1)
ElicitationHandler
(19-21)mcp/types.go (9)
JSONRPCRequest
(315-320)JSONRPCResponse
(329-333)MethodElicitationCreate
(60-60)ElicitationParams
(811-816)Params
(167-167)ElicitationRequest
(805-808)Request
(158-161)JSONRPC_VERSION
(113-113)Result
(236-240)client/transport/interface.go (2)
JSONRPCRequest
(50-55)JSONRPCResponse
(57-66)
server/stdio.go (3)
mcp/types.go (7)
ElicitationResult
(819-826)ElicitationRequest
(805-808)Params
(167-167)ElicitationParams
(811-816)JSONRPC_VERSION
(113-113)MethodElicitationCreate
(60-60)Result
(236-240)server/session.go (2)
ClientSession
(11-20)SessionWithElicitation
(52-56)server/sampling.go (1)
SessionWithSampling
(39-42)
🔇 Additional comments (49)
mcptest/mcptest.go (1)
145-145
: Cosmetic change approvedThe removal of the superfluous blank line is a harmless style cleanup and has no functional impact.
client/transport/sse_test.go (1)
408-459
: Whitespace-only change – no action needed.
No functional impact observed.client/transport/streamable_http_test.go (1)
418-451
: Whitespace-only change – no action needed.
The surrounding logic remains untouched.server/streamable_http_test.go (1)
846-897
: Cosmetic re-indentation – looks fine.
Behaviour of the header-passthrough test is unchanged.mcp/tools.go (1)
470-488
: Whitespace-only cleanup – no functional changes.Also applies to: 498-529
server/server.go (3)
178-178
: LGTM! Elicitation capability field follows established pattern.The addition of the
elicitation *bool
field to theserverCapabilities
struct correctly follows the same pattern as the existinglogging
capability field, using a pointer to bool for optional capability configuration.
285-290
: LGTM! Capability option function follows established pattern.The
WithElicitation()
function correctly implements the elicitation capability option following the exact same pattern asWithLogging()
, including proper use ofmcp.ToBoolPtr(true)
to set the capability flag.
578-580
: LGTM! Capability advertisement follows established pattern.The elicitation capability advertisement in
handleInitialize
correctly follows the same pattern as the logging capability, with proper nil and boolean checks before setting the capability to an empty struct.server/elicitation.go (1)
13-25
: LGTM! Method implementation follows established session patterns.The
RequestElicitation
method correctly implements the server-side elicitation request forwarding with proper session validation, interface type assertion, and error handling. The implementation follows the same pattern as other session-dependent server methods.client/elicitation.go (1)
11-19
: LGTM! Well-designed interface with comprehensive documentation.The
ElicitationHandler
interface is excellently designed with a clean single-method contract, proper type usage from the MCP protocol, and comprehensive documentation that clearly guides implementers on expected behavior including user interaction patterns.client/transport/inprocess.go (3)
16-16
: LGTM! Field addition follows established pattern.The
elicitationHandler
field correctly follows the same pattern as the existingsamplingHandler
field, using the appropriateserver.ElicitationHandler
interface type for in-process transport.
32-36
: LGTM! Option function follows established pattern.The
WithElicitationHandler
option function correctly follows the same pattern asWithSamplingHandler
, maintaining consistency in the transport configuration API.
59-60
: LGTM! Session creation logic properly handles both handler types.The updated session creation logic correctly uses OR condition to create sessions when either handler is present and properly passes both handlers to
NewInProcessSessionWithHandlers
, maintaining backward compatibility while enabling elicitation support.server/inprocess_session.go (5)
18-21
: LGTM! Interface definition follows established patterns.The
ElicitationHandler
interface correctly defines the server-side contract for elicitation handling with proper method signature, context support, and appropriate MCP protocol types.
30-30
: LGTM! Field addition follows established pattern.The
elicitationHandler
field correctly follows the same pattern as the existingsamplingHandler
field, maintaining consistency in the struct design.
42-49
: LGTM! Constructor follows established patterns.The
NewInProcessSessionWithHandlers
constructor correctly follows the same initialization pattern as the existing constructor while properly setting both handler fields, maintaining consistency and enabling dual handler support.
105-115
: LGTM! Method implementation mirrors established patterns.The
RequestElicitation
method correctly follows the exact same pattern asRequestSampling
with proper read locking, nil checking, and handler delegation, ensuring thread safety and consistent error handling.
128-128
: LGTM! Interface compliance assertion ensures correctness.The addition of
SessionWithElicitation
to the interface compliance assertions provides important compile-time verification thatInProcessSession
correctly implements the elicitation interface, following the established pattern of other interface assertions.server/elicitation_test.go (4)
12-57
: Well-structured mock implementations for testing.The mock session types correctly implement the expected interfaces and provide good test coverage infrastructure. The
mockBasicSession
properly represents sessions without elicitation support, whilemockElicitationSession
extends it with configurable elicitation behavior.
84-112
: Good test coverage for unsupported session scenario.This test correctly validates the error handling when a session doesn't support elicitation. The setup and assertions are appropriate.
114-172
: Comprehensive test for successful elicitation flow.This test effectively validates the happy path scenario with proper response validation and type checking. The mock setup and assertions are thorough.
174-263
: fakeSession type validatedThe table-driven tests reference
fakeSession
, which is defined inserver/server_test.go
, so there’s no missing type or compilation issue. Everything else inTestRequestElicitation
is well-structured and provides comprehensive coverage. Approving these changes.examples/elicitation/main.go (2)
1-13
: Clean package setup with appropriate imports.The import statements are well-organized and all dependencies are necessary for the example functionality.
99-99
: Appropriate use of atomic counter for request tracking.The atomic counter effectively demonstrates thread-safe state management across tool requests.
client/client.go (4)
26-26
: Consistent implementation following established patterns.The new
elicitationHandler
field andWithElicitationHandler
option function correctly follow the same patterns as the existing sampling functionality. The documentation is clear and implementation is straightforward.Also applies to: 46-52
166-169
: Proper capability declaration following existing pattern.The elicitation capability declaration correctly mirrors the sampling capability pattern, ensuring consistency in the client initialization flow.
443-443
: Clean integration with existing request routing.The elicitation request routing follows the established pattern and integrates seamlessly with the existing request handling infrastructure.
Also applies to: 448-449
503-549
: Well-implemented transport handler following established patterns.The
handleElicitationRequestTransport
method correctly mirrors the structure and patterns of the existinghandleSamplingRequestTransport
method. The parameter handling, error checking, and response construction all follow the established conventions.client/elicitation_test.go (4)
13-24
: Well-designed mock for flexible testing scenarios.The
mockElicitationHandler
provides good flexibility for testing different response scenarios and error conditions.
26-123
: Comprehensive test coverage with good scenario validation.The table-driven test effectively covers all important scenarios including error conditions and different response types. The assertion logic is thorough and appropriate.
125-135
: Simple and focused option test.The test correctly verifies that the
WithElicitationHandler
option properly sets the handler on the client.
192-225
: Complete and appropriate mock transport implementation.The mock transport provides all necessary interface methods with configurable behavior, making it suitable for testing various scenarios.
client/inprocess_elicitation_test.go (3)
12-33
: Effective mock handler for integration testing.The mock implementation provides appropriate tracking capabilities and simulates user acceptance, which is perfect for testing the end-to-end elicitation flow.
185-197
: Useful helper function for in-process elicitation testing.The helper function correctly sets up both server and client sides of elicitation handling for in-process testing scenarios.
199-206
: Simple and effective adapter pattern implementation.The wrapper correctly bridges the client and server elicitation handler interfaces, enabling seamless in-process testing.
mcp/types.go (8)
58-60
: LGTM! Method constant follows established patterns.The new elicitation method constant is well-documented and follows the existing naming convention for MCP methods.
455-456
: Client capability addition looks good.The elicitation capability follows the same pattern as other client capabilities like sampling.
485-486
: Server capability addition is consistent.The elicitation capability matches the client-side declaration and follows established patterns.
803-808
: Request type structure is well-designed.The
ElicitationRequest
follows the established pattern for MCP request types, properly embedding the baseRequest
type.
811-816
: Parameters are appropriately typed.The use of
any
forRequestedSchema
is correct for JSON Schema flexibility, consistent with other schema fields in the codebase.
819-826
: Result type is well-documented.The
ElicitationResult
structure properly embeds the baseResult
type and clearly documents all possible response scenarios.
829-835
: Response structure handles all cases correctly.The optional
Value
field withomitempty
tag is appropriate since it's only populated for accepted responses.
838-847
: Enum values cover all response scenarios.The three response types (accept, decline, cancel) appropriately handle all user interaction outcomes.
server/stdio.go (6)
55-64
: Session struct changes follow existing patterns.The
pendingElicitations
map mirrors the design ofpendingRequests
for sampling, maintaining consistency in the codebase.
73-77
: Response type matches established patterns.The
elicitationResponse
struct correctly mirrors thesamplingResponse
pattern with appropriate types.
185-246
: RequestElicitation implementation is solid.The method correctly implements the request-response pattern with proper cleanup, error handling, and context support, mirroring the sampling implementation.
256-260
: Interface implementation check added correctly.The compile-time assertion for
SessionWithElicitation
ensures the session properly implements the interface.
263-267
: Session instance initialization is complete.The
pendingElicitations
map is properly initialized alongside existing maps.
423-426
: Response routing added correctly.The elicitation response check follows the established pattern and is properly integrated into the message processing flow.
func TestClient_Initialize_WithElicitationHandler(t *testing.T) { | ||
mockTransport := &mockElicitationTransport{ | ||
sendRequestFunc: func(ctx context.Context, request transport.JSONRPCRequest) (*transport.JSONRPCResponse, error) { | ||
// Verify that elicitation capability is included | ||
// The client internally converts the typed params to a map for transport | ||
// So we check if we're getting the initialize request | ||
if request.Method != "initialize" { | ||
t.Fatalf("expected initialize method, got %s", request.Method) | ||
} | ||
|
||
// Return successful initialization response | ||
result := mcp.InitializeResult{ | ||
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, | ||
ServerInfo: mcp.Implementation{ | ||
Name: "test-server", | ||
Version: "1.0.0", | ||
}, | ||
Capabilities: mcp.ServerCapabilities{}, | ||
} | ||
|
||
resultBytes, _ := json.Marshal(result) | ||
return &transport.JSONRPCResponse{ | ||
ID: request.ID, | ||
Result: json.RawMessage(resultBytes), | ||
}, nil | ||
}, | ||
sendNotificationFunc: func(ctx context.Context, notification mcp.JSONRPCNotification) error { | ||
return nil | ||
}, | ||
} | ||
|
||
handler := &mockElicitationHandler{} | ||
client := NewClient(mockTransport, WithElicitationHandler(handler)) | ||
|
||
err := client.Start(context.Background()) | ||
if err != nil { | ||
t.Fatalf("failed to start client: %v", err) | ||
} | ||
|
||
_, err = client.Initialize(context.Background(), mcp.InitializeRequest{ | ||
Params: mcp.InitializeParams{ | ||
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, | ||
ClientInfo: mcp.Implementation{ | ||
Name: "test-client", | ||
Version: "1.0.0", | ||
}, | ||
Capabilities: mcp.ClientCapabilities{}, | ||
}, | ||
}) | ||
|
||
if err != nil { | ||
t.Fatalf("failed to initialize: %v", err) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Enhance test to verify elicitation capability declaration.
The test sets up client initialization properly but doesn't actually verify that elicitation capability is declared. The comment on line 140 mentions this verification, but it's not implemented.
mockTransport := &mockElicitationTransport{
sendRequestFunc: func(ctx context.Context, request transport.JSONRPCRequest) (*transport.JSONRPCResponse, error) {
- // Verify that elicitation capability is included
- // The client internally converts the typed params to a map for transport
- // So we check if we're getting the initialize request
if request.Method != "initialize" {
t.Fatalf("expected initialize method, got %s", request.Method)
}
+ // Verify that elicitation capability is included in the request
+ paramsBytes, err := json.Marshal(request.Params)
+ if err != nil {
+ t.Fatalf("failed to marshal params: %v", err)
+ }
+
+ var initParams struct {
+ Capabilities mcp.ClientCapabilities `json:"capabilities"`
+ }
+ if err := json.Unmarshal(paramsBytes, &initParams); err != nil {
+ t.Fatalf("failed to unmarshal params: %v", err)
+ }
+
+ if initParams.Capabilities.Elicitation == nil {
+ t.Error("expected elicitation capability to be declared")
+ }
+
// Return successful initialization response
result := mcp.InitializeResult{
This ensures the test actually verifies the capability declaration functionality.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
func TestClient_Initialize_WithElicitationHandler(t *testing.T) { | |
mockTransport := &mockElicitationTransport{ | |
sendRequestFunc: func(ctx context.Context, request transport.JSONRPCRequest) (*transport.JSONRPCResponse, error) { | |
// Verify that elicitation capability is included | |
// The client internally converts the typed params to a map for transport | |
// So we check if we're getting the initialize request | |
if request.Method != "initialize" { | |
t.Fatalf("expected initialize method, got %s", request.Method) | |
} | |
// Return successful initialization response | |
result := mcp.InitializeResult{ | |
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, | |
ServerInfo: mcp.Implementation{ | |
Name: "test-server", | |
Version: "1.0.0", | |
}, | |
Capabilities: mcp.ServerCapabilities{}, | |
} | |
resultBytes, _ := json.Marshal(result) | |
return &transport.JSONRPCResponse{ | |
ID: request.ID, | |
Result: json.RawMessage(resultBytes), | |
}, nil | |
}, | |
sendNotificationFunc: func(ctx context.Context, notification mcp.JSONRPCNotification) error { | |
return nil | |
}, | |
} | |
handler := &mockElicitationHandler{} | |
client := NewClient(mockTransport, WithElicitationHandler(handler)) | |
err := client.Start(context.Background()) | |
if err != nil { | |
t.Fatalf("failed to start client: %v", err) | |
} | |
_, err = client.Initialize(context.Background(), mcp.InitializeRequest{ | |
Params: mcp.InitializeParams{ | |
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, | |
ClientInfo: mcp.Implementation{ | |
Name: "test-client", | |
Version: "1.0.0", | |
}, | |
Capabilities: mcp.ClientCapabilities{}, | |
}, | |
}) | |
if err != nil { | |
t.Fatalf("failed to initialize: %v", err) | |
} | |
} | |
func TestClient_Initialize_WithElicitationHandler(t *testing.T) { | |
mockTransport := &mockElicitationTransport{ | |
sendRequestFunc: func(ctx context.Context, request transport.JSONRPCRequest) (*transport.JSONRPCResponse, error) { | |
if request.Method != "initialize" { | |
t.Fatalf("expected initialize method, got %s", request.Method) | |
} | |
// Verify that elicitation capability is included in the request | |
paramsBytes, err := json.Marshal(request.Params) | |
if err != nil { | |
t.Fatalf("failed to marshal params: %v", err) | |
} | |
var initParams struct { | |
Capabilities mcp.ClientCapabilities `json:"capabilities"` | |
} | |
if err := json.Unmarshal(paramsBytes, &initParams); err != nil { | |
t.Fatalf("failed to unmarshal params: %v", err) | |
} | |
if initParams.Capabilities.Elicitation == nil { | |
t.Error("expected elicitation capability to be declared") | |
} | |
// Return successful initialization response | |
result := mcp.InitializeResult{ | |
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, | |
ServerInfo: mcp.Implementation{ | |
Name: "test-server", | |
Version: "1.0.0", | |
}, | |
Capabilities: mcp.ServerCapabilities{}, | |
} | |
resultBytes, _ := json.Marshal(result) | |
return &transport.JSONRPCResponse{ | |
ID: request.ID, | |
Result: json.RawMessage(resultBytes), | |
}, nil | |
}, | |
sendNotificationFunc: func(ctx context.Context, notification mcp.JSONRPCNotification) error { | |
return nil | |
}, | |
} | |
handler := &mockElicitationHandler{} | |
client := NewClient(mockTransport, WithElicitationHandler(handler)) | |
err := client.Start(context.Background()) | |
if err != nil { | |
t.Fatalf("failed to start client: %v", err) | |
} | |
_, err = client.Initialize(context.Background(), mcp.InitializeRequest{ | |
Params: mcp.InitializeParams{ | |
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, | |
ClientInfo: mcp.Implementation{ | |
Name: "test-client", | |
Version: "1.0.0", | |
}, | |
Capabilities: mcp.ClientCapabilities{}, | |
}, | |
}) | |
if err != nil { | |
t.Fatalf("failed to initialize: %v", err) | |
} | |
} |
🤖 Prompt for AI Agents
In client/elicitation_test.go between lines 137 and 190, the test mentions
verifying that the elicitation capability is declared during client
initialization but does not actually perform this check. To fix this, add an
assertion in the mockTransport's sendRequestFunc to inspect the initialize
request's parameters and confirm that the elicitation capability is included as
expected. This will ensure the test validates the capability declaration
functionality properly.
Content: []mcp.Content{ | ||
mcp.TextContent{ | ||
Type: "text", | ||
Text: "Elicitation failed: " + err.Error(), | ||
}, | ||
}, | ||
IsError: true, | ||
}, nil | ||
} | ||
|
||
// Handle the response | ||
var responseText string | ||
switch result.Response.Type { | ||
case mcp.ElicitationResponseTypeAccept: | ||
responseText = "User accepted and provided data" | ||
case mcp.ElicitationResponseTypeDecline: | ||
responseText = "User declined to provide information" | ||
case mcp.ElicitationResponseTypeCancel: | ||
responseText = "User cancelled the request" | ||
} | ||
|
||
return &mcp.CallToolResult{ | ||
Content: []mcp.Content{ | ||
mcp.TextContent{ | ||
Type: "text", | ||
Text: responseText, | ||
}, | ||
}, | ||
}, nil | ||
}) | ||
|
||
// Create handler for elicitation | ||
mockHandler := &MockElicitationHandler{} | ||
|
||
// Create in-process client with elicitation handler | ||
client, err := NewInProcessClientWithElicitationHandler(mcpServer, mockHandler) | ||
if err != nil { | ||
t.Fatalf("Failed to create client: %v", err) | ||
} | ||
defer client.Close() | ||
|
||
// Start the client | ||
if err := client.Start(context.Background()); err != nil { | ||
t.Fatalf("Failed to start client: %v", err) | ||
} | ||
|
||
// Initialize the client | ||
_, err = client.Initialize(context.Background(), mcp.InitializeRequest{ | ||
Params: mcp.InitializeParams{ | ||
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, | ||
ClientInfo: mcp.Implementation{ | ||
Name: "test-client", | ||
Version: "1.0.0", | ||
}, | ||
Capabilities: mcp.ClientCapabilities{ | ||
Elicitation: &struct{}{}, | ||
}, | ||
}, | ||
}) | ||
if err != nil { | ||
t.Fatalf("Failed to initialize: %v", err) | ||
} | ||
|
||
// Call the tool that triggers elicitation | ||
result, err := client.CallTool(context.Background(), mcp.CallToolRequest{ | ||
Params: mcp.CallToolParams{ | ||
Name: "test_elicitation", | ||
Arguments: map[string]any{ | ||
"action": "test-action", | ||
}, | ||
}, | ||
}) | ||
|
||
if err != nil { | ||
t.Fatalf("Failed to call tool: %v", err) | ||
} | ||
|
||
// Verify the result | ||
if len(result.Content) == 0 { | ||
t.Fatal("Expected content in result") | ||
} | ||
|
||
textContent, ok := result.Content[0].(mcp.TextContent) | ||
if !ok { | ||
t.Fatal("Expected text content") | ||
} | ||
|
||
if textContent.Text != "User accepted and provided data" { | ||
t.Errorf("Unexpected result: %s", textContent.Text) | ||
} | ||
|
||
// Verify the handler was called | ||
if mockHandler.CallCount != 1 { | ||
t.Errorf("Expected handler to be called once, got %d", mockHandler.CallCount) | ||
} | ||
|
||
if mockHandler.LastRequest.Params.Message != "Need additional information for test-action" { | ||
t.Errorf("Unexpected elicitation message: %s", mockHandler.LastRequest.Params.Message) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix tool registration pattern to match current MCP server API.
The integration test provides excellent end-to-end coverage but uses an incorrect tool registration pattern.
// Add a tool that uses elicitation
- mcpServer.AddTool(mcp.Tool{
- Name: "test_elicitation",
- Description: "Test elicitation functionality",
- InputSchema: mcp.ToolInputSchema{
- Type: "object",
- Properties: map[string]any{
- "action": map[string]any{
- "type": "string",
- "description": "Action to perform",
- },
- },
- Required: []string{"action"},
- },
- }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
+ mcpServer.AddTool(
+ mcp.NewTool(
+ "test_elicitation",
+ mcp.WithDescription("Test elicitation functionality"),
+ mcp.WithString("action", mcp.Required(), mcp.Description("Action to perform")),
+ ),
+ func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
This matches the current MCP server API patterns used elsewhere in the codebase.
Otherwise, the test provides excellent coverage of the complete elicitation workflow from server request through client handling and back to server response.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
func TestInProcessElicitation(t *testing.T) { | |
// Create server with elicitation enabled | |
mcpServer := server.NewMCPServer("test-server", "1.0.0", server.WithElicitation()) | |
// Add a tool that uses elicitation | |
mcpServer.AddTool(mcp.Tool{ | |
Name: "test_elicitation", | |
Description: "Test elicitation functionality", | |
InputSchema: mcp.ToolInputSchema{ | |
Type: "object", | |
Properties: map[string]any{ | |
"action": map[string]any{ | |
"type": "string", | |
"description": "Action to perform", | |
}, | |
}, | |
Required: []string{"action"}, | |
}, | |
}, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
action, err := request.RequireString("action") | |
if err != nil { | |
return nil, err | |
} | |
// Create elicitation request | |
elicitationRequest := mcp.ElicitationRequest{ | |
Params: mcp.ElicitationParams{ | |
Message: "Need additional information for " + action, | |
RequestedSchema: map[string]interface{}{ | |
"type": "object", | |
"properties": map[string]interface{}{ | |
"confirm": map[string]interface{}{ | |
"type": "boolean", | |
"description": "Confirm the action", | |
}, | |
"details": map[string]interface{}{ | |
"type": "string", | |
"description": "Additional details", | |
}, | |
}, | |
"required": []string{"confirm"}, | |
}, | |
}, | |
} | |
// Request elicitation from client | |
result, err := mcpServer.RequestElicitation(ctx, elicitationRequest) | |
if err != nil { | |
return &mcp.CallToolResult{ | |
Content: []mcp.Content{ | |
mcp.TextContent{ | |
Type: "text", | |
Text: "Elicitation failed: " + err.Error(), | |
}, | |
}, | |
IsError: true, | |
}, nil | |
} | |
// Handle the response | |
var responseText string | |
switch result.Response.Type { | |
case mcp.ElicitationResponseTypeAccept: | |
responseText = "User accepted and provided data" | |
case mcp.ElicitationResponseTypeDecline: | |
responseText = "User declined to provide information" | |
case mcp.ElicitationResponseTypeCancel: | |
responseText = "User cancelled the request" | |
} | |
return &mcp.CallToolResult{ | |
Content: []mcp.Content{ | |
mcp.TextContent{ | |
Type: "text", | |
Text: responseText, | |
}, | |
}, | |
}, nil | |
}) | |
// Create handler for elicitation | |
mockHandler := &MockElicitationHandler{} | |
// Create in-process client with elicitation handler | |
client, err := NewInProcessClientWithElicitationHandler(mcpServer, mockHandler) | |
if err != nil { | |
t.Fatalf("Failed to create client: %v", err) | |
} | |
defer client.Close() | |
// Start the client | |
if err := client.Start(context.Background()); err != nil { | |
t.Fatalf("Failed to start client: %v", err) | |
} | |
// Initialize the client | |
_, err = client.Initialize(context.Background(), mcp.InitializeRequest{ | |
Params: mcp.InitializeParams{ | |
ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, | |
ClientInfo: mcp.Implementation{ | |
Name: "test-client", | |
Version: "1.0.0", | |
}, | |
Capabilities: mcp.ClientCapabilities{ | |
Elicitation: &struct{}{}, | |
}, | |
}, | |
}) | |
if err != nil { | |
t.Fatalf("Failed to initialize: %v", err) | |
} | |
// Call the tool that triggers elicitation | |
result, err := client.CallTool(context.Background(), mcp.CallToolRequest{ | |
Params: mcp.CallToolParams{ | |
Name: "test_elicitation", | |
Arguments: map[string]any{ | |
"action": "test-action", | |
}, | |
}, | |
}) | |
if err != nil { | |
t.Fatalf("Failed to call tool: %v", err) | |
} | |
// Verify the result | |
if len(result.Content) == 0 { | |
t.Fatal("Expected content in result") | |
} | |
textContent, ok := result.Content[0].(mcp.TextContent) | |
if !ok { | |
t.Fatal("Expected text content") | |
} | |
if textContent.Text != "User accepted and provided data" { | |
t.Errorf("Unexpected result: %s", textContent.Text) | |
} | |
// Verify the handler was called | |
if mockHandler.CallCount != 1 { | |
t.Errorf("Expected handler to be called once, got %d", mockHandler.CallCount) | |
} | |
if mockHandler.LastRequest.Params.Message != "Need additional information for test-action" { | |
t.Errorf("Unexpected elicitation message: %s", mockHandler.LastRequest.Params.Message) | |
} | |
} | |
// Add a tool that uses elicitation | |
mcpServer.AddTool( | |
mcp.NewTool( | |
"test_elicitation", | |
mcp.WithDescription("Test elicitation functionality"), | |
mcp.WithString("action", mcp.Required(), mcp.Description("Action to perform")), | |
), | |
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
action, err := request.RequireString("action") | |
if err != nil { | |
return nil, err | |
} | |
// Create elicitation request | |
elicitationRequest := mcp.ElicitationRequest{ | |
Params: mcp.ElicitationParams{ | |
Message: "Need additional information for " + action, | |
RequestedSchema: map[string]interface{}{ | |
"type": "object", | |
"properties": map[string]interface{}{ | |
"confirm": map[string]interface{}{ | |
"type": "boolean", | |
"description": "Confirm the action", | |
}, | |
"details": map[string]interface{}{ | |
"type": "string", | |
"description": "Additional details", | |
}, | |
}, | |
"required": []string{"confirm"}, | |
}, | |
}, | |
} | |
// Request elicitation from client | |
result, err := mcpServer.RequestElicitation(ctx, elicitationRequest) | |
if err != nil { | |
return &mcp.CallToolResult{ | |
Content: []mcp.Content{ | |
mcp.TextContent{ | |
Type: "text", | |
Text: "Elicitation failed: " + err.Error(), | |
}, | |
}, | |
IsError: true, | |
}, nil | |
} | |
// Handle the response | |
var responseText string | |
switch result.Response.Type { | |
case mcp.ElicitationResponseTypeAccept: | |
responseText = "User accepted and provided data" | |
case mcp.ElicitationResponseTypeDecline: | |
responseText = "User declined to provide information" | |
case mcp.ElicitationResponseTypeCancel: | |
responseText = "User cancelled the request" | |
} | |
return &mcp.CallToolResult{ | |
Content: []mcp.Content{ | |
mcp.TextContent{ | |
Type: "text", | |
Text: responseText, | |
}, | |
}, | |
}, nil | |
}, | |
) |
🤖 Prompt for AI Agents
In client/inprocess_elicitation_test.go from lines 35 to 183, the tool
registration uses an outdated pattern incompatible with the current MCP server
API. Update the tool registration to use the current API method by properly
defining the tool and its handler function according to the latest MCP server
conventions, ensuring the tool is added correctly to the server. This will align
the test with the rest of the codebase and maintain the integrity of the
elicitation workflow coverage.
// demoElicitationHandler demonstrates how to use elicitation in a tool | ||
func demoElicitationHandler(s *server.MCPServer) server.ToolHandlerFunc { | ||
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||
// Create an elicitation request to get project details | ||
elicitationRequest := mcp.ElicitationRequest{ | ||
Params: mcp.ElicitationParams{ | ||
Message: "I need some information to set up your project. Please provide the project details.", | ||
RequestedSchema: map[string]interface{}{ | ||
"type": "object", | ||
"properties": map[string]interface{}{ | ||
"projectName": map[string]interface{}{ | ||
"type": "string", | ||
"description": "Name of the project", | ||
"minLength": 1, | ||
}, | ||
"framework": map[string]interface{}{ | ||
"type": "string", | ||
"description": "Frontend framework to use", | ||
"enum": []string{"react", "vue", "angular", "none"}, | ||
}, | ||
"includeTests": map[string]interface{}{ | ||
"type": "boolean", | ||
"description": "Include test setup", | ||
"default": true, | ||
}, | ||
}, | ||
"required": []string{"projectName"}, | ||
}, | ||
}, | ||
} | ||
|
||
// Request elicitation from the client | ||
result, err := s.RequestElicitation(ctx, elicitationRequest) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to request elicitation: %w", err) | ||
} | ||
|
||
// Handle the user's response | ||
switch result.Response.Type { | ||
case mcp.ElicitationResponseTypeAccept: | ||
// User provided the information | ||
data, ok := result.Response.Value.(map[string]interface{}) | ||
if !ok { | ||
return nil, fmt.Errorf("unexpected response format") | ||
} | ||
|
||
projectName := data["projectName"].(string) | ||
framework := "none" | ||
if fw, ok := data["framework"].(string); ok { | ||
framework = fw | ||
} | ||
includeTests := true | ||
if tests, ok := data["includeTests"].(bool); ok { | ||
includeTests = tests | ||
} | ||
|
||
// Create project based on user input | ||
message := fmt.Sprintf( | ||
"Created project '%s' with framework: %s, tests: %v", | ||
projectName, framework, includeTests, | ||
) | ||
|
||
return &mcp.CallToolResult{ | ||
Content: []mcp.Content{ | ||
mcp.NewTextContent(message), | ||
}, | ||
}, nil | ||
|
||
case mcp.ElicitationResponseTypeDecline: | ||
return &mcp.CallToolResult{ | ||
Content: []mcp.Content{ | ||
mcp.NewTextContent("Project creation cancelled - user declined to provide information"), | ||
}, | ||
}, nil | ||
|
||
case mcp.ElicitationResponseTypeCancel: | ||
return nil, fmt.Errorf("project creation cancelled by user") | ||
|
||
default: | ||
return nil, fmt.Errorf("unexpected response type: %s", result.Response.Type) | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add safety checks for type assertions.
The example effectively demonstrates elicitation usage but has unsafe type assertions that could cause runtime panics.
// User provided the information
data, ok := result.Response.Value.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response format")
}
- projectName := data["projectName"].(string)
+ projectName, ok := data["projectName"].(string)
+ if !ok {
+ return nil, fmt.Errorf("projectName must be a string")
+ }
framework := "none"
if fw, ok := data["framework"].(string); ok {
framework = fw
}
includeTests := true
if tests, ok := data["includeTests"].(bool); ok {
includeTests = tests
}
This prevents potential runtime panics and provides better error messages for malformed responses.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
// demoElicitationHandler demonstrates how to use elicitation in a tool | |
func demoElicitationHandler(s *server.MCPServer) server.ToolHandlerFunc { | |
return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | |
// Create an elicitation request to get project details | |
elicitationRequest := mcp.ElicitationRequest{ | |
Params: mcp.ElicitationParams{ | |
Message: "I need some information to set up your project. Please provide the project details.", | |
RequestedSchema: map[string]interface{}{ | |
"type": "object", | |
"properties": map[string]interface{}{ | |
"projectName": map[string]interface{}{ | |
"type": "string", | |
"description": "Name of the project", | |
"minLength": 1, | |
}, | |
"framework": map[string]interface{}{ | |
"type": "string", | |
"description": "Frontend framework to use", | |
"enum": []string{"react", "vue", "angular", "none"}, | |
}, | |
"includeTests": map[string]interface{}{ | |
"type": "boolean", | |
"description": "Include test setup", | |
"default": true, | |
}, | |
}, | |
"required": []string{"projectName"}, | |
}, | |
}, | |
} | |
// Request elicitation from the client | |
result, err := s.RequestElicitation(ctx, elicitationRequest) | |
if err != nil { | |
return nil, fmt.Errorf("failed to request elicitation: %w", err) | |
} | |
// Handle the user's response | |
switch result.Response.Type { | |
case mcp.ElicitationResponseTypeAccept: | |
// User provided the information | |
data, ok := result.Response.Value.(map[string]interface{}) | |
if !ok { | |
return nil, fmt.Errorf("unexpected response format") | |
} | |
projectName := data["projectName"].(string) | |
framework := "none" | |
if fw, ok := data["framework"].(string); ok { | |
framework = fw | |
} | |
includeTests := true | |
if tests, ok := data["includeTests"].(bool); ok { | |
includeTests = tests | |
} | |
// Create project based on user input | |
message := fmt.Sprintf( | |
"Created project '%s' with framework: %s, tests: %v", | |
projectName, framework, includeTests, | |
) | |
return &mcp.CallToolResult{ | |
Content: []mcp.Content{ | |
mcp.NewTextContent(message), | |
}, | |
}, nil | |
case mcp.ElicitationResponseTypeDecline: | |
return &mcp.CallToolResult{ | |
Content: []mcp.Content{ | |
mcp.NewTextContent("Project creation cancelled - user declined to provide information"), | |
}, | |
}, nil | |
case mcp.ElicitationResponseTypeCancel: | |
return nil, fmt.Errorf("project creation cancelled by user") | |
default: | |
return nil, fmt.Errorf("unexpected response type: %s", result.Response.Type) | |
} | |
} | |
} | |
case mcp.ElicitationResponseTypeAccept: | |
// User provided the information | |
data, ok := result.Response.Value.(map[string]interface{}) | |
if !ok { | |
return nil, fmt.Errorf("unexpected response format") | |
} | |
projectName, ok := data["projectName"].(string) | |
if !ok { | |
return nil, fmt.Errorf("projectName must be a string") | |
} | |
framework := "none" | |
if fw, ok := data["framework"].(string); ok { | |
framework = fw | |
} | |
includeTests := true | |
if tests, ok := data["includeTests"].(bool); ok { | |
includeTests = tests | |
} | |
// Create project based on user input | |
message := fmt.Sprintf( | |
"Created project '%s' with framework: %s, tests: %v", | |
projectName, framework, includeTests, | |
) | |
return &mcp.CallToolResult{ | |
Content: []mcp.Content{ | |
mcp.NewTextContent(message), | |
}, | |
}, nil |
🤖 Prompt for AI Agents
In examples/elicitation/main.go from lines 15 to 97, the code uses unsafe type
assertions on the elicitation response data, which can cause runtime panics if
the data is malformed or missing expected fields. To fix this, add safety checks
by verifying the type assertions succeed before using the values, and return
clear error messages if the assertions fail. This includes checking that the
response value is a map[string]interface{} and that each expected field
(projectName, framework, includeTests) is of the correct type before accessing
them.
// Add a tool that uses elicitation | ||
mcpServer.AddTool( | ||
mcp.NewTool( | ||
"create_project", | ||
mcp.WithDescription("Creates a new project with user-specified configuration"), | ||
), | ||
demoElicitationHandler(mcpServer), | ||
) | ||
|
||
// Add another tool that demonstrates conditional elicitation | ||
mcpServer.AddTool( | ||
mcp.NewTool( | ||
"process_data", | ||
mcp.WithDescription("Processes data with optional user confirmation"), | ||
mcp.WithString("data", mcp.Required(), mcp.Description("Data to process")), | ||
), | ||
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { | ||
data := request.GetArguments()["data"].(string) | ||
|
||
// Only request elicitation if data seems sensitive | ||
if len(data) > 100 { | ||
elicitationRequest := mcp.ElicitationRequest{ | ||
Params: mcp.ElicitationParams{ | ||
Message: fmt.Sprintf("The data is %d characters long. Do you want to proceed with processing?", len(data)), | ||
RequestedSchema: map[string]interface{}{ | ||
"type": "object", | ||
"properties": map[string]interface{}{ | ||
"proceed": map[string]interface{}{ | ||
"type": "boolean", | ||
"description": "Whether to proceed with processing", | ||
}, | ||
"reason": map[string]interface{}{ | ||
"type": "string", | ||
"description": "Optional reason for your decision", | ||
}, | ||
}, | ||
"required": []string{"proceed"}, | ||
}, | ||
}, | ||
} | ||
|
||
result, err := mcpServer.RequestElicitation(ctx, elicitationRequest) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get confirmation: %w", err) | ||
} | ||
|
||
if result.Response.Type != mcp.ElicitationResponseTypeAccept { | ||
return &mcp.CallToolResult{ | ||
Content: []mcp.Content{ | ||
mcp.NewTextContent("Processing cancelled by user"), | ||
}, | ||
}, nil | ||
} | ||
|
||
responseData := result.Response.Value.(map[string]interface{}) | ||
if proceed, ok := responseData["proceed"].(bool); !ok || !proceed { | ||
reason := "No reason provided" | ||
if r, ok := responseData["reason"].(string); ok && r != "" { | ||
reason = r | ||
} | ||
return &mcp.CallToolResult{ | ||
Content: []mcp.Content{ | ||
mcp.NewTextContent(fmt.Sprintf("Processing declined: %s", reason)), | ||
}, | ||
}, nil | ||
} | ||
} | ||
|
||
// Process the data | ||
processed := fmt.Sprintf("Processed %d characters of data", len(data)) | ||
count := requestCount.Add(1) | ||
|
||
return &mcp.CallToolResult{ | ||
Content: []mcp.Content{ | ||
mcp.NewTextContent(fmt.Sprintf("%s (request #%d)", processed, count)), | ||
}, | ||
}, nil | ||
}, | ||
) | ||
|
||
// Create and start stdio server | ||
stdioServer := server.NewStdioServer(mcpServer) | ||
|
||
// Handle graceful shutdown | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
defer cancel() | ||
|
||
sigChan := make(chan os.Signal, 1) | ||
signal.Notify(sigChan, os.Interrupt) | ||
|
||
go func() { | ||
<-sigChan | ||
cancel() | ||
}() | ||
|
||
fmt.Fprintln(os.Stderr, "Elicitation demo server started") | ||
if err := stdioServer.Listen(ctx, os.Stdin, os.Stdout); err != nil { | ||
log.Fatal(err) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Fix unsafe type assertions in the second tool.
The main function provides an excellent comprehensive example, but contains unsafe type assertions that could cause runtime panics.
func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
- data := request.GetArguments()["data"].(string)
+ dataRaw, exists := request.GetArguments()["data"]
+ if !exists {
+ return nil, fmt.Errorf("data argument is required")
+ }
+ data, ok := dataRaw.(string)
+ if !ok {
+ return nil, fmt.Errorf("data argument must be a string")
+ }
// Only request elicitation if data seems sensitive
if len(data) > 100 {
// ... elicitation request setup ...
if result.Response.Type != mcp.ElicitationResponseTypeAccept {
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent("Processing cancelled by user"),
},
}, nil
}
- responseData := result.Response.Value.(map[string]interface{})
+ responseData, ok := result.Response.Value.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf("unexpected response format")
+ }
if proceed, ok := responseData["proceed"].(bool); !ok || !proceed {
reason := "No reason provided"
if r, ok := responseData["reason"].(string); ok && r != "" {
reason = r
}
return &mcp.CallToolResult{
Content: []mcp.Content{
mcp.NewTextContent(fmt.Sprintf("Processing declined: %s", reason)),
},
}, nil
}
}
This makes the example more robust and demonstrates proper error handling patterns for users.
🤖 Prompt for AI Agents
In examples/elicitation/main.go from lines 101 to 208, the code uses unsafe type
assertions on the "data" argument and the elicitation response value, which can
cause runtime panics if the types do not match. To fix this, replace direct type
assertions with type checks using the "ok" idiom to verify the type before using
the value. If the type check fails, return an appropriate error to handle the
unexpected type safely and prevent panics.
id, err := response.ID.Int64() | ||
if err != nil { | ||
return false | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing validation check for response structure.
The condition should also verify that either Result
or Error
is present, similar to the sampling response handler.
Apply this diff to add the missing validation:
- if err != nil {
+ if err != nil || (response.Result == nil && response.Error == nil) {
return false
}
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
id, err := response.ID.Int64() | |
if err != nil { | |
return false | |
} | |
id, err := response.ID.Int64() | |
if err != nil || (response.Result == nil && response.Error == nil) { | |
return false | |
} |
🤖 Prompt for AI Agents
In server/stdio.go around lines 540 to 543, the code extracts the ID from
response but lacks validation to ensure the response structure contains either a
Result or an Error field. To fix this, add a condition that checks if both
Result and Error are nil or absent, and return false in that case, similar to
the sampling response handler. This ensures the response is valid before
proceeding.
* Add ElicitationRequest, ElicitationResult, and related types to mcp/types.go * Implement server-side RequestElicitation method with session support * Add client-side ElicitationHandler interface and request handling * Implement elicitation in stdio and in-process transports * Add comprehensive tests following sampling patterns * Create elicitation example demonstrating usage patterns * Use 'Elicitation' prefix for type names to maintain clarity
e215c98
to
c0b1fd7
Compare
Any idea when this might be getting in? It would be great to have elicitation. |
hey when we are planning to merge this ? |
@dugenkui03 or @ezynda3 can you review when you have a chance? Thanks! |
This has a bunch of merge conflicts that need to be addressed |
Description
This PR implements MCP elicitation support, allowing servers to request additional information from clients during interactions. The implementation follows the same patterns as the existing sampling feature, providing bidirectional communication capabilities for stdio and in-process transports.
Fixes #413
Type of Change
Checklist
MCP Spec Compliance
-- we do not implement response schema validation in the server
Additional Information
Implementation Details
ElicitationRequest
,ElicitationResult
,ElicitationParams
,ElicitationResponse
, andElicitationResponseType
WithElicitation()
option andRequestElicitation()
methodElicitationHandler
interface andWithElicitationHandler()
optionDesign Decisions
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Documentation