Skip to content

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

semistrict
Copy link

@semistrict semistrict commented Jul 21, 2025

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

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • MCP spec compatibility implementation
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Code refactoring (no functional changes)
  • Performance improvement
  • Tests only (no functional changes)
  • Other (please describe):

Checklist

  • My code follows the code style of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • I have updated the documentation accordingly

MCP Spec Compliance

  • This PR implements a feature defined in the MCP specification
  • Link to relevant spec section: Elicitation Concepts
  • Implementation follows the specification exactly
    -- we do not implement response schema validation in the server

Additional Information

Implementation Details

  • Added core types: ElicitationRequest, ElicitationResult, ElicitationParams, ElicitationResponse, and ElicitationResponseType
  • Server capability: WithElicitation() option and RequestElicitation() method
  • Client capability: ElicitationHandler interface and WithElicitationHandler() option
  • Transport support: Implemented in stdio (bidirectional) and in-process transports
  • Testing: Comprehensive tests following the same patterns as sampling tests
  • Example: Complete example demonstrating elicitation usage patterns

Design Decisions

  • Architecture: Followed existing sampling implementation patterns closely for consistency

Summary by CodeRabbit

  • New Features

    • Introduced elicitation capability, allowing servers to request additional structured input from clients with customizable messages and validation schemas.
    • Added support for elicitation requests and responses in both client and server, including capability negotiation during initialization.
    • Provided example tools demonstrating elicitation flows for project creation and data processing.
  • Bug Fixes

    • None.
  • Tests

    • Added comprehensive unit and integration tests covering elicitation request handling, client and server configuration, and end-to-end flows.
  • Documentation

    • Example program included to showcase elicitation usage and integration.

Copy link
Contributor

coderabbitai bot commented Jul 21, 2025

Walkthrough

This 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

Files/Paths Change Summary
client/client.go, client/elicitation.go Add elicitation handler support, client option, request routing, and handler interface.
client/elicitation_test.go, client/inprocess_elicitation_test.go Add unit and integration tests for elicitation handling and flows.
client/transport/inprocess.go Add elicitation handler to in-process transport and update session creation logic.
examples/elicitation/main.go Add example server demonstrating elicitation-enabled tools and workflows.
mcp/types.go Define elicitation request/result types, constants, and capability flags.
server/elicitation.go, server/elicitation_test.go Implement and test server-side elicitation request forwarding.
server/inprocess_session.go Add elicitation handler support and request method to in-process sessions.
server/server.go Add elicitation capability flag and option to server initialization.
server/session.go Add SessionWithElicitation interface for session-level elicitation support.
server/stdio.go Implement elicitation request/response handling in stdio server/session.

Estimated code review effort

4 (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

  • Implement sampling in Stdio #461: Implements sampling support in the client with similar capability declaration, handler field, client option, and request routing as this PR does for elicitation, showing a direct code-level relationship in extending client bidirectional request handling.

Suggested labels

type: enhancement, area: sdk

Suggested reviewers

  • pottekkat
  • rwjblue-glean

📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e215c98 and c0b1fd7.

📒 Files selected for processing (13)
  • 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)
  • examples/elicitation/main.go (1 hunks)
  • mcp/types.go (4 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)
🚧 Files skipped from review as they are similar to previous changes (13)
  • server/inprocess_session.go
  • server/session.go
  • server/elicitation.go
  • client/elicitation.go
  • client/elicitation_test.go
  • server/elicitation_test.go
  • server/server.go
  • examples/elicitation/main.go
  • client/transport/inprocess.go
  • server/stdio.go
  • client/inprocess_elicitation_test.go
  • mcp/types.go
  • client/client.go
✨ Finishing Touches
  • 📝 Generate Docstrings

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need 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)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a 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 on ctx.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 the WithElicitation() 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

📥 Commits

Reviewing files that changed from the base of the PR and between baa7153 and e215c98.

📒 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 approved

The 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 the serverCapabilities struct correctly follows the same pattern as the existing logging 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 as WithLogging(), including proper use of mcp.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 existing samplingHandler field, using the appropriate server.ElicitationHandler interface type for in-process transport.


32-36: LGTM! Option function follows established pattern.

The WithElicitationHandler option function correctly follows the same pattern as WithSamplingHandler, 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 existing samplingHandler 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 as RequestSampling 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 that InProcessSession 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, while mockElicitationSession 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 validated

The table-driven tests reference fakeSession, which is defined in server/server_test.go, so there’s no missing type or compilation issue. Everything else in TestRequestElicitation 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 and WithElicitationHandler 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 existing handleSamplingRequestTransport 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 base Request type.


811-816: Parameters are appropriately typed.

The use of any for RequestedSchema 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 base Result type and clearly documents all possible response scenarios.


829-835: Response structure handles all cases correctly.

The optional Value field with omitempty 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 of pendingRequests for sampling, maintaining consistency in the codebase.


73-77: Response type matches established patterns.

The elicitationResponse struct correctly mirrors the samplingResponse 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.

Comment on lines +137 to +190
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)
}
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +35 to +183
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)
}
}
Copy link
Contributor

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.

Suggested change
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.

Comment on lines +15 to +97
// 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)
}
}
}
Copy link
Contributor

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.

Suggested change
// 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.

Comment on lines +101 to +208
// 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)
}
}
Copy link
Contributor

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.

Comment on lines +540 to +543
id, err := response.ID.Int64()
if err != nil {
return false
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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
@miguelb-gk
Copy link

Any idea when this might be getting in? It would be great to have elicitation.

@Sameer-Jain-123
Copy link

hey when we are planning to merge this ?

@JBUinfo
Copy link

JBUinfo commented Aug 14, 2025

@dugenkui03 or @ezynda3 can you review when you have a chance? Thanks!

@ezynda3
Copy link
Contributor

ezynda3 commented Aug 14, 2025

This has a bunch of merge conflicts that need to be addressed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Task: Add Elicitation Support
5 participants