Understanding Elicitation

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

In the previous lessons, your tool executions were “fire-and-forget.” The LLM (or client) sent a request, the tool ran its logic, and immediately returned a result.

But what happens when a tool needs to perform a high-stakes operation, like deleting a database table or transferring money? You don’t want the server to execute that immediately. You want it to pause, ask the user for confirmation or extra details, and only then proceed.

This capability is called Elicitation.

Elicitation allows an MCP Server to ask the Client for input during the execution of a tool. It turns a one-way command into a two-way conversation.

The Compatibility Challenge

It is important to understand that Elicitation is a sophisticated feature that requires explicit support from the Host application.

The Interaction Flow

Here is how the flow works when the Host application supports Elicitation:

Implementing the Server

You will now create a system that simulates a database manager. It will allow read-only queries freely but will trigger an Elicitation flow for destructive commands.

from pydantic import BaseModel, Field
from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("SafeDB-Manager", host="127.0.0.1", port=8000)

class SafetyVerification(BaseModel):
    """Schema for validating critical operations."""
    confirm: bool = Field(
        description="Must be True to proceed with the operation."
    )
    justification: str = Field(
        description="A mandatory reason for why this critical operation is being performed."
    )
    environment: str = Field(
        description="The environment you intend to affect (Development, Staging, or Production)."
    )

@mcp.tool()
async def execute_query(query: str, ctx: Context) -> str:
    """
    Executes a database query.
    Triggers a safety check (elicitation) for destructive commands.
    """
    query_upper = query.upper().strip()

    is_destructive = any(cmd in query_upper for cmd in ["DROP", "DELETE", "TRUNCATE"])

    if is_destructive:
        result = await ctx.elicit(
            message=f"CRITICAL WARNING: You are attempting to run a destructive query: '{query}'. Verification required.",
            schema=SafetyVerification
        )

        if result.action == "accept" and result.data:
            data = result.data

            if not data.confirm:
                return "Operation blocked: User did not confirm."

            if data.environment == "Production" and len(data.justification) < 10:
                return "Operation blocked: Production changes require a detailed justification (min 10 chars)."

            return (
                f"Query Executed Successfully on [{data.environment}].\n"
                f"   Query: {query}\n"
                f"   Log Reason: {data.justification}"
            )

        elif result.action == "decline":
            return "Operation declined by user."

        else:
            return "Operation cancelled."

    return f"Read-only query executed: {query}"

if __name__ == "__main__":
    print("Starting SafeDB Server on http://127.0.0.1:8000/mcp ...")
    mcp.run(transport="streamable-http")

What You Are Building

You are building a tiny server that pauses on dangerous SQL and asks the client to confirm. The client becomes the “human approval” step and sends structured input back to the server before it proceeds.

Key Concepts in db_server.py:

  • ctx: Context: Note the new argument in execute_query. By adding ctx: Context, FastMCP automatically injects the connection context, giving you access to special methods like elicit.
  • SafetyVerification: We use Pydantic to define exactly what we need from the user. We aren’t just asking “Yes or No”; we are enforcing structured data (Environment, Justification, Confirmation).
  • await ctx.elicit(...): This is the core logic. It sends a message and the schema to the client. The Python function execution creates a checkpoint here and waits.

How the Server Script Works

  • FastMCP(...) starts the MCP server and exposes tools over streamable-http.
  • SafetyVerification defines the exact input the user must provide.
  • ctx.elicit(...) pauses the tool and asks the client for that input.
  • After the client replies, the tool resumes and validates the response before returning a final result.

Implementing the Custom Client

Since standard AI clients might not support this flow yet, you must write a custom client. This client will act as the Host application, intercepting the server’s request and asking you (the user) for input via the terminal.

import asyncio
import json
from mcp import ClientSession, types
from mcp.client.session import RequestContext
from mcp.client.streamable_http import streamablehttp_client

SERVER_URL = "http://127.0.0.1:8000/mcp"

async def elicitation_handler(
    context: RequestContext,
    params: types.ElicitRequestParams
) -> types.ElicitResult:
    """
    This function is called automatically when the Server triggers ctx.elicit().
    """
    print("\n" + "!" * 50)
    print(f"SERVER MESSAGE: {params.message}")
    print("!" * 50 + "\n")

    print("Please provide the required safety details:")

    print("Select Environment (Development/Staging/Production):")
    env_input = input("> ").strip().title()

    print("Justification for this action:")
    reason_input = input("> ").strip()

    print("Type 'yes' to confirm execution:")
    confirm_input = input("> ").lower().strip()
    is_confirmed = (confirm_input == "yes")

    user_response_data = {
        "confirm": is_confirmed,
        "justification": reason_input,
        "environment": env_input
    }

    return types.ElicitResult(
        action="accept",
        content=user_response_data
    )

async def run_client():
    print(f"Connecting to DB Server at {SERVER_URL}...")

    async with streamablehttp_client(SERVER_URL) as (read, write, _):
        async with ClientSession(
            read,
            write,
            elicitation_callback=elicitation_handler
        ) as session:
            await session.initialize()
            print("Connected.\n")

            print("--- Test 1: Running Safe Query ---")
            result_safe = await session.call_tool(
                "execute_query",
                arguments={"query": "SELECT * FROM users"}
            )
            print(f"Result: {result_safe.content[0].text}\n")

            print("--- Test 2: Running DESTRUCTIVE Query ---")
            print("Sending: DROP TABLE invoices...")

            result_dangerous = await session.call_tool(
                "execute_query",
                arguments={"query": "DROP TABLE invoices"}
            )

            print("\n--- Final Result from Server ---")
            if result_dangerous.isError:
                 print(f"Error: {result_dangerous.content}")
            else:
                 print(result_dangerous.content[0].text)

if __name__ == "__main__":
    asyncio.run(run_client())

Key Concepts in db_client.py:

  • elicitation_handler: This is a standalone asynchronous function. It receives the message from the server. In a real desktop app, this would open a modal window. In this script, it uses input() to pause the terminal and ask you for data.
  • elicitation_callback=elicitation_handler: When initializing the ClientSession, you must register your handler. This is the crucial step that “enables” elicitation support for your client.

How the Client Script Works

  • elicitation_handler(...) is the callback the server triggers when it calls ctx.elicit(...).
  • It collects user input via input() and returns a structured response that matches SafetyVerification.
  • ClientSession(..., elicitation_callback=...) is what makes the client “elicitation-aware.”

Run It

  1. In one terminal, start the server:
uv run python db_server.py
uv run python db_client.py
See forum comments
Download course materials from Github
Previous: Introduction Next: Handling Elicitation with a Custom Client