Skip to content

feat: implement MCP elicitation support (#413) #548

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 5 commits into
base: main
Choose a base branch
from

Conversation

JBUinfo
Copy link

@JBUinfo JBUinfo commented Aug 14, 2025

Exactly this PR #495 but fixing conflicts

Fixes #413

Summary by CodeRabbit

  • New Features

    • First-class elicitation between clients and servers: interactive prompts with Accept/Decline/Cancel responses; advertised capability and cross-transport support (stdio, in-process) for requesting and responding to elicitation.
  • Documentation

    • New example demonstrating project creation and multi-step confirmations via elicitation, including conditional follow-up prompts.
  • Tests

    • Comprehensive client, server, in-process and stdio elicitation tests covering success, decline, cancel, error and initialization scenarios.

* 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
Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

Walkthrough

Adds bidirectional elicitation across MCP: protocol types, client ElicitationHandler and option, server RequestElicitation and ServerOption, transport/session plumbing (stdio & in-process), tests, and an example demonstrating accept/decline/cancel flows and schema-bearing requests.

Changes

Cohort / File(s) Summary
MCP protocol types
mcp/types.go
Adds MethodElicitationCreate, client/server Elicitation capability flags, and types: ElicitationRequest, ElicitationParams, ElicitationResult, ElicitationResponse, ElicitationResponseType (+ `accept
Client core & API
client/client.go, client/elicitation.go
Adds ElicitationHandler interface, elicitationHandler field on Client, WithElicitationHandler ClientOption, capability advertisement during Initialize, and transport-level handling of elicitation/create.
Client tests & in-process integration
client/elicitation_test.go, client/inprocess_elicitation_test.go
Adds unit and in-process tests covering no-handler error, accept/decline/cancel flows, option wiring, Initialize flow, and an in-process adapter wrapper and helper.
Client transport (in-process)
client/transport/inprocess.go
Adds elicitationHandler field and WithElicitationHandler option; uses NewInProcessSessionWithHandlers when sampling or elicitation handlers provided.
Server API & options
server/server.go, server/elicitation.go
Adds WithElicitation() ServerOption, internal capability flag, and MCPServer.RequestElicitation delegating to the active session.
Server session interfaces & in-process session
server/session.go, server/inprocess_session.go
Adds SessionWithElicitation interface, extends InProcessSession with elicitationHandler, adds NewInProcessSessionWithHandlers, and RequestElicitation on session. (Note: duplicate SessionWithElicitation declaration present.)
Server stdio session
server/stdio.go
Implements RequestElicitation on stdioSession, pending-elicitation tracking, response routing/handling, and asserts stdioSession implements SessionWithElicitation.
Server tests
server/elicitation_test.go
Adds tests for RequestElicitation: no-session error, unsupported-session error, successful accept/decline flows, and table-driven scenarios.
Examples
examples/elicitation/main.go
New example demonstrating elicitation flows (project creation, confirmation) with accept/decline/cancel handling via stdio server.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Assessment against linked issues

Objective Addressed Explanation
Add ElicitRequest and ElicitResult types (#413)
Implement elicitation capability in server (#413)
Add client-side elicitation handling (#413)
Support for structured elicitation with request schemas (#413)

Out-of-scope changes

Code Change Explanation
Duplicate SessionWithElicitation interface declaration (server/session.go) Two identical SessionWithElicitation declarations were added side-by-side in server/session.go; appears accidental and unrelated to the stated objectives.

Possibly related PRs

Suggested labels

type: enhancement, area: mcp spec, area: sdk

Suggested reviewers

  • pottekkat
  • rwjblue-glean
  • robert-jackson-glean

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 46e18dd and 2841d97.

📒 Files selected for processing (2)
  • client/elicitation_test.go (1 hunks)
  • examples/elicitation/main.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/elicitation/main.go
  • client/elicitation_test.go
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.
    • 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.
  • 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 the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

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

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • 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

🔭 Outside diff range comments (6)
client/transport/inprocess.go (4)

57-66: Ensure sessionID is generated before creating/registering the session

If NewInProcessTransport() (without options) is used, sessionID remains empty. Creating a session with an empty ID can break RegisterSession/UnregisterSession lookups and GetSessionId. Generate an ID when missing and persist it.

Apply this diff:

 func (c *InProcessTransport) Start(ctx context.Context) error {
-	// Create and register session if we have handlers
-	if c.samplingHandler != nil || c.elicitationHandler != nil {
-		c.session = server.NewInProcessSessionWithHandlers(c.sessionID, c.samplingHandler, c.elicitationHandler)
+	// Create and register session if we have handlers
+	if c.samplingHandler != nil || c.elicitationHandler != nil {
+		// Ensure we have a session ID
+		if c.sessionID == "" {
+			c.sessionID = c.server.GenerateInProcessSessionID()
+		}
+		c.session = server.NewInProcessSessionWithHandlers(c.sessionID, c.samplingHandler, c.elicitationHandler)
 		if err := c.server.RegisterSession(ctx, c.session); err != nil {
 			return fmt.Errorf("failed to register session: %w", err)
 		}
 	}
 	return nil
 }

94-103: Preserve session context for notifications

Notifications should also carry the client session in the context to keep behavior consistent with requests (logging level, client info, etc.).

Apply this diff:

 func (c *InProcessTransport) SendNotification(ctx context.Context, notification mcp.JSONRPCNotification) error {
 	notificationBytes, err := json.Marshal(notification)
 	if err != nil {
 		return fmt.Errorf("failed to marshal notification: %w", err)
 	}
 	notificationBytes = append(notificationBytes, '\n')
+	// Add session to context if available
+	if c.session != nil {
+		ctx = c.server.WithContext(ctx, c.session)
+	}
 	c.server.HandleMessage(ctx, notificationBytes)

 	return nil
 }

111-116: Use actual session ID when unregistering

If sessionID wasn’t set on construction, UnregisterSession with an empty ID will fail. Fall back to the session’s ID.

Apply this diff:

 func (c *InProcessTransport) Close() error {
 	if c.session != nil {
-		c.server.UnregisterSession(context.Background(), c.sessionID)
+		sid := c.sessionID
+		if sid == "" {
+			sid = c.session.SessionID()
+		}
+		c.server.UnregisterSession(context.Background(), sid)
 	}
 	return nil
 }

118-120: Fix: Return actual session ID from GetSessionId()

Currently always returns an empty string.

Apply this diff:

 func (c *InProcessTransport) GetSessionId() string {
-	return ""
+	if c.sessionID != "" {
+		return c.sessionID
+	}
+	if c.session != nil {
+		return c.session.SessionID()
+	}
+	return ""
 }
server/stdio.go (2)

223-225: Same stdout write race in RequestSampling

Same issue as RequestElicitation: direct writes to the shared stdout without coordinating with the server’s write lock. Use the same writeWithGlobalLock helper to serialize all writes.

Apply this diff:

- if _, err := writer.Write(requestBytes); err != nil {
-   return nil, fmt.Errorf("failed to write sampling request: %w", err)
- }
+ if _, err := writeWithGlobalLock(writer, requestBytes); err != nil {
+   return nil, fmt.Errorf("failed to write sampling request: %w", err)
+ }

695-701: Unify server write path with the same global write lock to avoid interleaving with session-originated requests

writeResponse currently uses a different mutex (s.writeMu) than session-originated requests. To guarantee atomic frames across all writers, route all writes through the same global lock.

Apply this diff:

- // Protect concurrent writes
- s.writeMu.Lock()
- defer s.writeMu.Unlock()
-
- // Write response followed by newline
- if _, err := fmt.Fprintf(writer, "%s\n", responseBytes); err != nil {
-   return err
- }
+ // Write response followed by newline under shared lock
+ if _, err := writeWithGlobalLock(writer, append(responseBytes, '\n')); err != nil {
+   return err
+ }

Optionally, remove s.writeMu if no other code depends on it after this change.

🧹 Nitpick comments (17)
mcp/types.go (3)

59-62: Introduce elicitation method: looks correct and consistent with naming.

Constant name and value are consistent with the rest of the method enum. Non-blocking nit: consider aligning the comment URL to the spec path once a canonical spec page exists.


834-841: Prefer a more precise type for RequestedSchema to improve safety and interop.

Using any works, but you’ll lose type guarantees and may incur unnecessary reflection. Two better options:

  • Use json.RawMessage to preserve arbitrary JSON without reflection.
  • If the repo already standardizes on a JSON Schema type (e.g., the Tool InputSchema handling per our past learnings), reuse that type for consistency.

Suggested minimal change:

-    RequestedSchema any `json:"requestedSchema"`
+    RequestedSchema json.RawMessage `json:"requestedSchema"`

842-860: Response envelope is well modeled; consider adding a helper validator.

The tri-state response with optional Value is clear. Consider adding a small helper (outside this file) to validate that Value is present iff Type is “accept” and to enforce schema-validation errors vs user-decline/cancel semantics. This will make downstream handler code simpler and safer.

server/session.go (1)

55-61: SessionWithElicitation interface is clear; document cancel vs error semantics.

Consider clarifying in a comment when to return an ElicitationResult with Type=cancel vs returning an error (e.g., transport interruption vs user-initiated cancel). This avoids ambiguity for implementers across transports.

client/elicitation.go (1)

9-19: ElicitationHandler contract looks good; add guidance on cancel/error handling.

Interface is minimal and clear. Non-blocking: add a sentence that user-initiated cancellations should produce a result with Type=cancel, while transport or context cancellations should return an error. This aligns client behavior with server expectations.

server/elicitation.go (1)

10-25: Server-side elicitation request routing is sound

Correctly retrieves the client session from context and delegates only if SessionWithElicitation is implemented. Error messages match tests.

Consider defining exported sentinel errors for "no active session" and "session does not support elicitation" to enable callers to handle them programmatically without string matching in the future. Keep current messages for test compatibility.

client/client.go (1)

527-573: Transport-level elicitation handling is correct and mirrors sampling

  • Validates handler presence
  • Parses params safely via JSON round-trip
  • Constructs mcp.ElicitationRequest and returns properly marshaled result

Sampling and elicitation transport handlers share identical request-param parsing and response-marshaling patterns. Consider extracting a small generic helper to reduce duplication and error-prone maintenance (example skeleton):

// outside the changed ranges – illustrative
func (c *Client) handleBidi[TParams any, TResult any](
    ctx context.Context,
    req transport.JSONRPCRequest,
    build func(TParams) (any, error), // constructs the MCP request to call the handler with
    call func(context.Context, any) (TResult, error), // invokes the specific handler
) (*transport.JSONRPCResponse, error) {
    var params TParams
    if req.Params != nil {
        b, err := json.Marshal(req.Params)
        if err != nil {
            return nil, fmt.Errorf("failed to marshal params: %w", err)
        }
        if err := json.Unmarshal(b, &params); err != nil {
            return nil, fmt.Errorf("failed to unmarshal params: %w", err)
        }
    }
    mcpReq, err := build(params)
    if err != nil {
        return nil, err
    }
    result, err := call(ctx, mcpReq)
    if err != nil {
        return nil, err
    }
    rb, err := json.Marshal(result)
    if err != nil {
        return nil, fmt.Errorf("failed to marshal result: %w", err)
    }
    return &transport.JSONRPCResponse{
        JSONRPC: mcp.JSONRPC_VERSION,
        ID:      req.ID,
        Result:  json.RawMessage(rb),
    }, nil
}

This keeps both flows uniform and easier to extend (e.g., future bidirectional methods).

server/elicitation_test.go (1)

114-172: Happy-path test validates Accept flow and structured Value

Asserts type and content of response payload; representative example of structured elicitation.

You could add an additional sub-assert that Value conforms to the declared schema in the test (e.g., check both fields exist and are strings) to better document intended behavior.

server/stdio.go (3)

629-683: Tighten response discrimination to avoid false positives

If a message has an id but neither result nor error, this method will parse, attempt to unmarshal nil, and treat it as an elicitation response error. Align this with handleSamplingResponse by ignoring such frames.

Apply this diff:

- 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
+ }

423-446: Potential goroutine leak on context cancel in readNextLine

When ctx is cancelled, this returns "", nil but leaves the spawned goroutine blocked on ReadString. This can leak goroutines until stdin closes. Consider refactoring to avoid a goroutine per read or to unblock the reader on cancel (e.g., by using an interruptible reader, piping through an io.Pipe on shutdown, or a single long-lived goroutine that you signal to stop).

I can draft a refactor using a single read goroutine feeding a channel that the loop selects on, with proper shutdown semantics.


327-333: Nit: comment contradicts behavior

Comment says “Default to discarding logs” but logs are directed to os.Stderr.

Apply this diff to clarify:

- ), // Default to discarding logs
+ ),

Or update the comment to match the behavior.

client/inprocess_elicitation_test.go (5)

23-33: Prefer map[string]any over map[string]interface{} for consistency

Throughout the repository, any is used. Keep tests consistent to avoid noise in diffs and improve readability.

Apply this diff:

-       Value: map[string]interface{}{
+       Value: map[string]any{
          "response": "User provided data",
          "accepted": true,
        },

39-53: Minor: use map[string]any consistently in schema definitions

The file mixes map[string]any and map[string]interface{}. Align on any.

Apply this diff:

-   Properties: map[string]any{
-     "action": map[string]any{
+   Properties: map[string]any{
+     "action": map[string]any{
        "type":        "string",
        "description": "Action to perform",
      },
    },

And within the elicitation request:

- RequestedSchema: map[string]interface{}{
+ RequestedSchema: map[string]any{
     "type": "object",
-    "properties": map[string]interface{}{
-      "confirm": map[string]interface{}{
+    "properties": map[string]any{
+      "confirm": map[string]any{
         "type":        "boolean",
         "description": "Confirm the action",
       },
-      "details": map[string]interface{}{
+      "details": map[string]any{
         "type":        "string",
         "description": "Additional details",
       },
     },

193-197: Prefer public option to set handler rather than mutating unexported field

Setting client.elicitationHandler directly couples the test to internal details. Use the functional option to exercise public API.

Apply this diff:

- client := NewClient(inProcessTransport)
- client.elicitationHandler = handler
+ client := NewClient(inProcessTransport, WithElicitationHandler(handler))

80-93: Coverage: add a failing elicitation path test

The tool path already returns a CallToolResult with IsError true when RequestElicitation fails. Add a test that injects a failing handler or forces RequestElicitation to error and assert the error content and IsError=true.

I can add a subtest that makes the handler return an error and asserts the tool returns a text error and IsError=true.


171-173: String stability

Tests compare exact strings. Consider asserting on the response type (Accept/Decline/Cancel) and presence of substrings to reduce brittleness to minor text changes.

I can adjust the assertion to check Contains("User accepted") instead of full equality if desired.

client/elicitation_test.go (1)

84-123: Strengthen assertions by checking ID echo and response payload

After unmarshalling, also assert that the response type matches the test case and that the Request/Response IDs are echoed back.

For example:

if result != nil {
  if result.ID != request.ID {
    t.Errorf("expected response ID %v, got %v", request.ID, result.ID)
  }
  var elicitationResult mcp.ElicitationResult
  if err := json.Unmarshal(result.Result, &elicitationResult); err != nil {
    t.Errorf("failed to unmarshal result: %v", err)
  } else if tt.expectedError == "" && elicitationResult.Response.Type == "" {
    t.Error("empty response type")
  }
}
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9f16336 and f149690.

📒 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 (4 hunks)
  • server/server.go (3 hunks)
  • server/session.go (1 hunks)
  • server/stdio.go (6 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-03-04T06:59:43.882Z
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.

Applied to files:

  • mcp/types.go
📚 Learning: 2025-06-30T07:13:17.052Z
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.

Applied to files:

  • server/server.go
🧬 Code Graph Analysis (11)
client/elicitation.go (1)
mcp/types.go (2)
  • ElicitationRequest (829-832)
  • ElicitationResult (843-850)
server/session.go (1)
mcp/types.go (2)
  • ElicitationRequest (829-832)
  • ElicitationResult (843-850)
client/elicitation_test.go (5)
mcp/types.go (18)
  • ElicitationResult (843-850)
  • ElicitationRequest (829-832)
  • ElicitationResponse (853-859)
  • ElicitationResponseTypeAccept (866-866)
  • ElicitationResponseTypeDecline (868-868)
  • ElicitationResponseTypeCancel (870-870)
  • JSONRPCRequest (329-334)
  • NewRequestId (263-265)
  • MethodElicitationCreate (61-61)
  • Params (181-181)
  • Result (250-254)
  • InitializeResult (434-448)
  • Implementation (506-509)
  • ServerCapabilities (476-503)
  • JSONRPCNotification (337-340)
  • InitializeRequest (418-422)
  • InitializeParams (424-430)
  • ClientCapabilities (459-471)
client/elicitation.go (1)
  • ElicitationHandler (11-19)
server/inprocess_session.go (1)
  • ElicitationHandler (19-21)
client/client.go (3)
  • Client (17-29)
  • WithElicitationHandler (50-54)
  • NewClient (71-81)
client/transport/inprocess.go (1)
  • WithElicitationHandler (32-36)
server/elicitation.go (2)
mcp/types.go (2)
  • ElicitationRequest (829-832)
  • ElicitationResult (843-850)
server/session.go (2)
  • ClientSessionFromContext (82-87)
  • SessionWithElicitation (56-60)
server/inprocess_session.go (4)
mcp/types.go (3)
  • ElicitationRequest (829-832)
  • ElicitationResult (843-850)
  • JSONRPCNotification (337-340)
client/elicitation.go (1)
  • ElicitationHandler (11-19)
server/session.go (4)
  • ClientSession (11-20)
  • SessionWithLogging (23-29)
  • SessionWithClientInfo (43-53)
  • SessionWithElicitation (56-60)
server/sampling.go (1)
  • SessionWithSampling (42-45)
client/client.go (5)
client/elicitation.go (1)
  • ElicitationHandler (11-19)
server/inprocess_session.go (1)
  • ElicitationHandler (19-21)
client/transport/inprocess.go (1)
  • WithElicitationHandler (32-36)
mcp/types.go (10)
  • JSONRPCRequest (329-334)
  • JSONRPCResponse (343-347)
  • MethodSamplingCreateMessage (877-877)
  • MethodElicitationCreate (61-61)
  • ElicitationParams (835-840)
  • Params (181-181)
  • ElicitationRequest (829-832)
  • Request (172-175)
  • JSONRPC_VERSION (115-115)
  • Result (250-254)
client/transport/interface.go (2)
  • JSONRPCRequest (57-62)
  • JSONRPCResponse (64-73)
server/elicitation_test.go (4)
mcp/types.go (9)
  • JSONRPCNotification (337-340)
  • ElicitationResult (843-850)
  • ElicitationRequest (829-832)
  • Params (181-181)
  • ElicitationParams (835-840)
  • ElicitationResponse (853-859)
  • ElicitationResponseTypeAccept (866-866)
  • ElicitationResponseType (862-862)
  • ElicitationResponseTypeDecline (868-868)
server/server.go (2)
  • NewMCPServer (307-333)
  • WithElicitation (293-297)
mcp/utils.go (1)
  • ToBoolPtr (861-863)
server/session.go (1)
  • ClientSession (11-20)
server/server.go (1)
mcp/utils.go (1)
  • ToBoolPtr (861-863)
client/transport/inprocess.go (1)
server/inprocess_session.go (4)
  • SamplingHandler (14-16)
  • ElicitationHandler (19-21)
  • InProcessSession (23-33)
  • NewInProcessSessionWithHandlers (43-50)
client/inprocess_elicitation_test.go (7)
mcp/types.go (13)
  • ElicitationRequest (829-832)
  • ElicitationResult (843-850)
  • ElicitationResponse (853-859)
  • ElicitationResponseTypeAccept (866-866)
  • Params (181-181)
  • ElicitationParams (835-840)
  • Content (941-943)
  • TextContent (947-954)
  • TextContent (956-956)
  • ElicitationResponseTypeDecline (868-868)
  • ElicitationResponseTypeCancel (870-870)
  • LATEST_PROTOCOL_VERSION (105-105)
  • ClientCapabilities (459-471)
server/server.go (2)
  • WithElicitation (293-297)
  • MCPServer (144-169)
mcp/tools.go (5)
  • Tool (557-572)
  • Description (851-855)
  • ToolInputSchema (612-617)
  • Properties (1093-1097)
  • Required (859-863)
client/elicitation.go (1)
  • ElicitationHandler (11-19)
server/inprocess_session.go (1)
  • ElicitationHandler (19-21)
client/client.go (3)
  • Client (17-29)
  • WithElicitationHandler (50-54)
  • NewClient (71-81)
client/transport/inprocess.go (2)
  • NewInProcessTransportWithOptions (44-55)
  • WithElicitationHandler (32-36)
server/stdio.go (3)
mcp/types.go (8)
  • JSONRPCNotification (337-340)
  • ElicitationResult (843-850)
  • ElicitationRequest (829-832)
  • Params (181-181)
  • ElicitationParams (835-840)
  • JSONRPC_VERSION (115-115)
  • MethodElicitationCreate (61-61)
  • Result (250-254)
server/session.go (2)
  • ClientSession (11-20)
  • SessionWithElicitation (56-60)
server/sampling.go (1)
  • SessionWithSampling (42-45)
🔇 Additional comments (27)
mcp/types.go (4)

469-471: Client capability flag added appropriately.

Using a pointer to an empty struct mirrors existing capability flags (e.g., Sampling) and keeps the wire format minimal. No issues.


501-503: Server capability flag added appropriately.

Matches the client-side shape and is correctly nullable for opt-in behavior in initialize.


825-832: ElicitationRequest wrapper shape is fine and consistent with other request types.

Named Params field is consistent with many existing request wrappers in this file (e.g., ReadResourceRequest). Good placement and JSON tags.


864-871: Enum values are reasonable and easy to consume.

No issues with the response type constants.

server/server.go (3)

180-186: Capability plumbing extended with elicitation (opt-in).

Adding elicitation *bool aligns with the existing pattern (e.g., logging, sampling). Looks good.


639-642: Initialize advertises elicitation correctly when enabled.

Consistent with other capabilities. No concerns.


292-298: Elicitation option: end-to-end implementation verified

WithElicitation is OK — the server method, session interface + implementations, method constant, and tests are present.

  • server/elicitation.go: func (s *MCPServer) RequestElicitation(...)
  • server/session.go: RequestElicitation declared on the Session interface
  • server/stdio.go, server/inprocess_session.go: concrete RequestElicitation implementations
  • mcp/types.go: MethodElicitationCreate = "elicitation/create"
  • Tests: server/elicitation_test.go, client/elicitation_test.go, client/inprocess_elicitation_test.go
examples/elicitation/main.go (1)

46-50: RequestElicitation usage looks good.

The example demonstrates the intended request/response flow and handles all response types. Once the safety checks above are added, this is a solid showcase.

client/transport/inprocess.go (2)

14-18: Addition of elicitation handler field looks correct

Wires in elicitation support alongside sampling in the in-process transport.


32-36: New option for elicitation handler is consistent with sampling

Public option mirrors existing WithSamplingHandler pattern. No issues.

client/client.go (4)

28-29: Client now tracks an elicitation handler

Field addition is minimal and consistent with sampling.


48-55: WithElicitationHandler option is correct and documented

Mirrors the sampling option with appropriate capability advertisement note.


179-183: Initialize advertises elicitation capability when handler is set

This aligns with capability negotiation and the new bidirectional request type.


467-474: Incoming request switch extended to handle elicitation

Approach is consistent with sampling; default branch remains defensive.

server/inprocess_session.go (5)

18-22: ElicitationHandler interface is well-scoped

Clear separation of concerns and symmetric with SamplingHandler.


31-33: Session now tracks elicitation handler

Read/write guarded by RWMutex in RequestElicitation; good thread-safety practice.


43-50: Constructor with both handlers improves clarity

NewInProcessSessionWithHandlers simplifies wiring from transports/tests.


119-129: RequestElicitation correctly delegates to handler

Uses read lock, nil-check, and returns handler output. Error message consistent with tests.


138-143: Interface compliance assertions are complete

Catches regressions at compile time.

server/elicitation_test.go (2)

84-112: Unsupported-session test reads cleanly and validates exact error

Uses WithElicitation option and a session without elicitation support. Solid coverage.


174-239: No action needed — fakeSession is defined in tests

The repository contains type fakeSession at server/server_test.go:1802 (found by rg). The original concern about an undefined type is incorrect — do not apply the suggested diff.

  • server/server_test.go:1802 — type fakeSession struct { ... }

Likely an incorrect or invalid review comment.

server/stdio.go (3)

302-307: LGTM: safe writer swap

Using defer to unlock improves exception-safety during writer updates.


317-321: LGTM: initialized pending maps for sampling and elicitation

Prevents nil map writes at runtime.


520-524: Ordering is fine; responses are intercepted before server handling

Processing sampling and elicitation responses prior to routing to the server avoids misclassification and unnecessary work.

client/inprocess_elicitation_test.go (1)

115-123: Nice: end-to-end in-process setup validates elicitation plumbing

Creating a real MCP server with a tool that triggers elicitation exercises the path realistically.

client/elicitation_test.go (2)

26-78: Good breadth of cases

Table covers no-handler, accept/decline/cancel, and handler error propagation. Nicely scoped.


192-226: Mock transport shape is sufficient

Start/Close/no-op notification handler are adequate for these tests.

Comment on lines +239 to +300
// RequestElicitation sends an elicitation request to the client and waits for the response.
func (s *stdioSession) RequestElicitation(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error) {
s.mu.RLock()
writer := s.writer
s.mu.RUnlock()

if writer == nil {
return nil, fmt.Errorf("no writer available for sending requests")
}

// Generate a unique request ID
id := s.requestID.Add(1)

// Create a response channel for this request
responseChan := make(chan *elicitationResponse, 1)
s.pendingMu.Lock()
s.pendingElicitations[id] = responseChan
s.pendingMu.Unlock()

// Cleanup function to remove the pending request
cleanup := func() {
s.pendingMu.Lock()
delete(s.pendingElicitations, id)
s.pendingMu.Unlock()
}
defer cleanup()

// Create the JSON-RPC request
jsonRPCRequest := struct {
JSONRPC string `json:"jsonrpc"`
ID int64 `json:"id"`
Method string `json:"method"`
Params mcp.ElicitationParams `json:"params"`
}{
JSONRPC: mcp.JSONRPC_VERSION,
ID: id,
Method: string(mcp.MethodElicitationCreate),
Params: request.Params,
}

// Marshal and send the request
requestBytes, err := json.Marshal(jsonRPCRequest)
if err != nil {
return nil, fmt.Errorf("failed to marshal elicitation request: %w", err)
}
requestBytes = append(requestBytes, '\n')

if _, err := writer.Write(requestBytes); err != nil {
return nil, fmt.Errorf("failed to write elicitation request: %w", err)
}

// Wait for the response or context cancellation
select {
case <-ctx.Done():
return nil, ctx.Err()
case response := <-responseChan:
if response.err != nil {
return nil, response.err
}
return response.result, nil
}
}
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 14, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Critical: concurrent writes to stdout are not synchronized in RequestElicitation, risking interleaved/broken JSON

RequestElicitation writes directly to the shared writer without coordinating with the server's write lock. Meanwhile, server responses and notifications use StdioServer.writeResponse with a different lock (s.writeMu). This can interleave bytes across concurrent writes (sampling requests, elicitation requests, server responses, notifications), corrupting frames.

Use a single shared lock for all writes to the underlying stdout writer.

Apply this diff to write under a shared lock:

- if _, err := writer.Write(requestBytes); err != nil {
-   return nil, fmt.Errorf("failed to write elicitation request: %w", err)
- }
+ if _, err := writeWithGlobalLock(writer, requestBytes); err != nil {
+   return nil, fmt.Errorf("failed to write elicitation request: %w", err)
+ }

Additionally, add a shared write helper and mutex (outside the selected range):

// At package level (near other vars)
var stdioWriteMu sync.Mutex

// writeWithGlobalLock ensures all writes to the stdio transport are serialized.
func writeWithGlobalLock(w io.Writer, b []byte) (int, error) {
  stdioWriteMu.Lock()
  defer stdioWriteMu.Unlock()
  return w.Write(b)
}

And update writeResponse and RequestSampling to use the same helper (see separate comments).

🤖 Prompt for AI Agents
In server/stdio.go around lines 239 to 300, RequestElicitation writes directly
to the shared writer without using the global stdio write lock, which can
interleave bytes with other writers; add a package-level mutex stdioWriteMu and
helper writeWithGlobalLock(w io.Writer, b []byte) that locks, writes and
unlocks, then replace the direct writer.Write(requestBytes) call with
writeWithGlobalLock(writer, requestBytes); also ensure writeResponse and
RequestSampling are updated to use the same helper so all stdout writes are
serialized.

Copy link
Author

Choose a reason for hiding this comment

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

already using s.mu.RLock()

Copy link
Contributor

Choose a reason for hiding this comment

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

@JBUinfo You're right that s.mu.RLock() is being used, but it only protects access to the writer field itself, not the actual write operation to stdout.

The issue is that once RequestElicitation gets the writer reference (safely with s.mu.RLock()), it then calls writer.Write(requestBytes) without any coordination with other parts of the system that also write to stdout.

Meanwhile, StdioServer.writeResponse uses a separate s.writeMu lock for its writes. This means:

  1. RequestElicitation writes to stdout (unprotected write operation)
  2. writeResponse writes to stdout (protected by s.writeMu)
  3. These can happen concurrently, potentially interleaving bytes and corrupting JSON frames

The s.mu.RLock() protects the field access but not the I/O operation itself. We need all writes to stdout to use the same synchronization mechanism to prevent concurrent writes from corrupting the output stream.

ฅ(•ﻌ•)ฅ


✏️ Learnings added
Learnt from: JBUinfo
PR: mark3labs/mcp-go#548
File: server/stdio.go:239-300
Timestamp: 2025-08-14T16:35:34.070Z
Learning: In server/stdio.go, the s.mu mutex protects access to the writer field, but does not protect the actual write operations to stdout. Concurrent write operations to the same stdout stream need to be synchronized using the same mutex to prevent byte interleaving and JSON corruption.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

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: 1

🧹 Nitpick comments (1)
server/session.go (1)

55-61: Interface addition is appropriate; clarify cancellation semantics and fix doc grammar.

Good place for the elicitation extension. Tighten the comment to specify context behavior and correct wording.

-// SessionWithElicitation is an extension of ClientSession that can send elicitation requests
+// SessionWithElicitation is an extension of ClientSession that can send elicitation requests.
 type SessionWithElicitation interface {
 	ClientSession
-	// RequestElicitation sends an elicitation request to the client and waits for response
+	// RequestElicitation sends an elicitation request to the client and waits for a response.
+	// Implementations MUST honor ctx cancellation/deadline and return an error consistent with ctx.Err().
 	RequestElicitation(ctx context.Context, request mcp.ElicitationRequest) (*mcp.ElicitationResult, error)
 }

Optionally, add a typed accessor to reduce repetitive type assertions at call sites:

// SessionWithElicitationFromContext retrieves a SessionWithElicitation from context, if available.
func SessionWithElicitationFromContext(ctx context.Context) (SessionWithElicitation, bool) {
	cs := ClientSessionFromContext(ctx)
	swe, ok := cs.(SessionWithElicitation)
	return swe, ok
}
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f149690 and 3573372.

📒 Files selected for processing (1)
  • server/session.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
server/session.go (1)
mcp/types.go (2)
  • ElicitationRequest (829-832)
  • ElicitationResult (843-850)

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: 2

♻️ Duplicate comments (3)
examples/elicitation/main.go (3)

56-60: Good: guarded assertion prevents panics on malformed elicitation response.

This addresses prior feedback about unsafe map assertions and avoids runtime panics.


121-131: Good: switched to safe parameter access.

Using request.GetString("data", "") avoids unsafe type assertions and is robust when params are missing.


165-168: Don’t proceed on malformed accept payload; fail fast.

Falling back to an empty map will treat malformed responses as declines/missing values. Return an error to make issues explicit.

-				responseData, ok := result.Response.Value.(map[string]interface{})
-				if !ok {
-					responseData = make(map[string]interface{})
-				}
+				responseData, ok := result.Response.Value.(map[string]interface{})
+				if !ok {
+					return nil, fmt.Errorf("unexpected response format")
+				}
🧹 Nitpick comments (4)
examples/elicitation/main.go (4)

22-42: Tighten schema: disallow extraneous fields.

Add additionalProperties: false so clients don’t send unexpected fields.

 				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,
 						},
 					},
+					"additionalProperties": false,
 					"required": []string{"projectName"},
 				},

157-163: Differentiate Decline vs Cancel; return an error on cancellation.

Lumping all non-accept responses into “cancelled” loses semantics and forces clients to parse text. Treat Decline as a non-error result and Cancel as an error.

-				if result.Response.Type != mcp.ElicitationResponseTypeAccept {
-					return &mcp.CallToolResult{
-						Content: []mcp.Content{
-							mcp.NewTextContent("Processing cancelled by user"),
-						},
-					}, nil
-				}
+				switch result.Response.Type {
+				case mcp.ElicitationResponseTypeAccept:
+					// continue to validate fields below
+				case mcp.ElicitationResponseTypeDecline:
+					return &mcp.CallToolResult{
+						Content: []mcp.Content{
+							mcp.NewTextContent("Processing declined by user"),
+						},
+					}, nil
+				case mcp.ElicitationResponseTypeCancel:
+					return nil, fmt.Errorf("processing cancelled by user")
+				default:
+					return nil, fmt.Errorf("unexpected response type: %s", result.Response.Type)
+				}

136-149: Tighten schema for confirmation request.

Disallow extra fields to keep the contract tight.

 						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",
 								},
 							},
+							"additionalProperties": false,
 							"required": []string{"proceed"},
 						},

202-204: Handle SIGTERM for graceful shutdown in more environments.

Add syscall.SIGTERM to support container orchestrators.

-	signal.Notify(sigChan, os.Interrupt)
+	signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

Additional change outside the selected range:

// in the imports block
"syscall"
📜 Review details

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

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8bc9f9c and 46e18dd.

📒 Files selected for processing (2)
  • client/elicitation_test.go (1 hunks)
  • examples/elicitation/main.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/elicitation_test.go
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-20T20:39:51.870Z
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.

Applied to files:

  • examples/elicitation/main.go
🧬 Code Graph Analysis (1)
examples/elicitation/main.go (5)
server/server.go (4)
  • MCPServer (144-169)
  • ToolHandlerFunc (41-41)
  • NewMCPServer (307-333)
  • WithElicitation (293-297)
mcp/tools.go (2)
  • CallToolRequest (54-58)
  • CallToolResult (40-51)
mcp/types.go (7)
  • ElicitationRequest (829-832)
  • Params (181-181)
  • ElicitationParams (835-840)
  • ElicitationResponseTypeAccept (866-866)
  • Content (941-943)
  • ElicitationResponseTypeDecline (868-868)
  • ElicitationResponseTypeCancel (870-870)
mcp/utils.go (1)
  • NewTextContent (199-204)
server/stdio.go (1)
  • NewStdioServer (325-336)

Comment on lines +170 to +181
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
}
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

Validate required field ‘proceed’ on accept path.

Since the schema requires proceed, treat missing/invalid types as an error rather than silently declining.

-				if proceed, ok := responseData["proceed"].(bool); !ok || !proceed {
+				proceed, ok := responseData["proceed"].(bool)
+				if !ok {
+					return nil, fmt.Errorf("invalid or missing 'proceed' in elicitation response")
+				}
+				if !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
 				}
📝 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
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
}
proceed, ok := responseData["proceed"].(bool)
if !ok {
return nil, fmt.Errorf("invalid or missing 'proceed' in elicitation response")
}
if !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
}
🤖 Prompt for AI Agents
In examples/elicitation/main.go around lines 170 to 180, the code currently
treats a missing or non-boolean "proceed" value as a silent decline; because the
schema requires "proceed", validate that responseData contains a boolean
"proceed" and if it's missing or the type is invalid return an explicit error
(e.g. fmt.Errorf with a clear message) instead of building a CallToolResult
decline; if "proceed" is present and false you may still return the existing
declined CallToolResult, but ensure missing/invalid values short-circuit with an
error response so callers can detect schema violations.

@JBUinfo JBUinfo force-pushed the simistrict-elicitation branch from 46e18dd to 2841d97 Compare August 14, 2025 17:13
@JBUinfo
Copy link
Author

JBUinfo commented Aug 14, 2025

Hi @ezynda3 !
I have resolved the conflicts and applied a couple of "coderabit" changes

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
2 participants