विषय पर बढ़ें

Elicitation

मशीनी अनुवाद

यह page अंग्रेज़ी documentation से अपने-आप अनुवादित किया गया है, और अंग्रेज़ी page ही प्रामाणिक version है। अगर कुछ गलत लगे, तो अनुवाद page बताता है कि इसकी सूचना कैसे दें।

जो tool अपना काम आधा कर चुका हो और उसके पास बस एक जवाब की कमी हो, उसका fail होना ज़रूरी नहीं।

Elicitation उसे पूछने देता है। tool call के बीच में user को एक सवाल मिलता है, और उसका जवाब उसी function call में वापस आ जाता है।

इसके दो mode हैं:

  • Form mode: आपको एक value चाहिए (confirmation, तारीख, मात्रा)। आप fields बताते हैं, client form render करता है।
  • URL mode: आपको user को कहीं और भेजना है (OAuth consent screen, payment page)। user वहाँ जो कुछ भी करता है, वह protocol से होकर नहीं गुज़रता।

और पूछने के दो तरीके हैं। जिसे पहले अपनाना चाहिए वह है resolver: आप सवाल को एक parameter पर टाँग देते हैं, और SDK पूछ लेता है - किसी भी connection पर, client चाहे किसी भी protocol पीढ़ी का हो। सीधा तरीका, await ctx.elicit(...), server से client को जाने वाली request है, एक ऐसा channel जो सिर्फ़ legacy connection (spec version 2025-11-25 या उससे पहले) वाले client के लिए ही मौजूद होता है। दोनों इस page पर हैं; resolver से शुरू करें।

resolver से पूछना

जो सवाल पूरे tool को रोके रखता है - पक्का? तीन मिलते-जुलते accounts में से कौन-सा? - उसे tool body से निकालकर resolver में रखा जा सकता है, और framework उसे आपके लिए पूछ लेता है।

Annotated[T, Resolve(fn)] से annotate किया गया parameter tool body से पहले fn चलाकर भरा जाता है। जब resolver को value पहले से पता हो तो वह उसे सीधे लौटाता है, वरना Elicit(...) लौटाता है ताकि framework पूछ ले:

server.py
from typing import Annotated

from pydantic import BaseModel

from mcp.server import MCPServer
from mcp.server.mcpserver import (
    AcceptedElicitation,
    CancelledElicitation,
    DeclinedElicitation,
    Elicit,
    ElicitationResult,
    Resolve,
)

mcp = MCPServer("Files")

_FOLDERS: dict[str, list[str]] = {"/tmp/empty": [], "/tmp/project": ["main.py", "README.md"]}


class Confirm(BaseModel):
    ok: bool


async def confirm_delete(path: str) -> Confirm | Elicit[Confirm]:
    """Resolver: ask for confirmation only when the folder is not empty."""
    file_count = len(_FOLDERS.get(path, []))
    if file_count == 0:
        return Confirm(ok=True)  # nothing to confirm, no round-trip to the client
    return Elicit(f"{path} has {file_count} file(s). Delete anyway?", Confirm)


@mcp.tool()
async def delete_folder(
    path: str,
    confirm: Annotated[ElicitationResult[Confirm], Resolve(confirm_delete)],
) -> str:
    """Delete a folder, asking for confirmation when it is not empty."""
    match confirm:
        case AcceptedElicitation(data=Confirm(ok=True)):
            _FOLDERS.pop(path, None)
            return f"deleted {path}"
        case AcceptedElicitation():
            return "kept the folder"
        case DeclinedElicitation():
            return "declined: folder not deleted"
        case CancelledElicitation():
            return "cancelled: folder not deleted"
  • confirm_delete tool के अपने path argument को नाम से पढ़ता है, folder की सूची बनाता है, और सिर्फ़ तभी elicit करता है जब ज़रूरी हो - खाली folder client तक एक भी round trip के बिना Confirm(ok=True) में resolve हो जाता है।
  • delete_folder ElicitationResult[Confirm] annotate करता है, इसलिए framework पूरा नतीजा inject करता है और tool हर स्थिति को match करता है: accept-and-confirm, accept-but-keep (ok=False), decline, cancel।
  • confirm parameter tool के input schema में कभी नहीं दिखता - client path देता है, resolver confirm देता है।

जब tool को branch करने की ज़रूरत न हो तो इसके बजाय unwrapped model (Annotated[Confirm, Resolve(confirm_delete)]) annotate करें: accept पर उसे model मिलता है और decline या cancel पर call एक error के साथ abort हो जाता है।

resolver हर connection पर काम करता है। legacy connection वाले client को SDK सवाल सीधे भेजता है; 2026-07-28 connection पर SDK call से सवाल लौटाता है, और client की अगली कोशिश जवाब साथ लाती है। आपके resolver को फ़र्क कभी पता नहीं चलता; नीचे जो होता है, वह Multi-round-trip requests है।

पूछना तो resolver के कामों में से सिर्फ़ एक है। सामान्य तंत्र - बिना पूछे compute होने वाली dependencies, dependencies की dependencies, model क्या दे सकता है और क्या नहीं - Dependencies page पर है।

tool के अंदर से पूछना

tool अपनी body के बीच में रुककर भी पूछ सकता है।

Warning

ctx.elicit() और ctx.elicit_url() server से client को जाने वाली requests हैं - एक ऐसा channel जो सिर्फ़ legacy connection (spec version 2025-11-25 या उससे पहले) वाले client के लिए मौजूद होता है। 2026-07-28 connection पर server की ओर से शुरू की गई कोई request नहीं होती, इसलिए ये calls fail हो जाते हैं। resolver दोनों पर काम करता है। पूरी जानकारी Protocol versions में है।

await ctx.elicit() एक message और एक Pydantic model लेता है:

server.py
from pydantic import BaseModel, Field

from mcp.server import MCPServer
from mcp.server.mcpserver import Context

mcp = MCPServer("Bistro")


class AlternativeDate(BaseModel):
    accept_alternative: bool = Field(description="Try another date?")
    date: str = Field(default="2025-12-26", description="Alternative date (YYYY-MM-DD)")


@mcp.tool()
async def book_table(date: str, party_size: int, ctx: Context) -> str:
    """Book a table at the bistro."""
    if date != "2025-12-25":
        return f"Booked a table for {party_size} on {date}."

    result = await ctx.elicit(
        message=f"No tables for {party_size} on {date}. Would you like to try another date?",
        schema=AlternativeDate,
    )
    if result.action == "accept" and result.data.accept_alternative:
        return await book_table(result.data.date, party_size, ctx)
    return "No booking made."
  • Context parameter ही आपको ctx.elicit देता है; कोई भी tool इसे ले सकता है। उस object का अपना page है: Context
  • AlternativeDate उस जवाब का schema है जो आप चाहते हैं।
  • tool async def है। होना ही चाहिए: यह बीच में रुककर किसी इंसान का इंतज़ार करता है।
  • किसी भी दूसरी तारीख पर tool तुरंत लौट आता है। यह सिर्फ़ तभी पूछता है जब ज़रूरी हो।
  • user जो तारीख accept करता है, वह book_table से ही होकर वापस जाती है। जवाब भी बाकी input की तरह input ही है: अगर विकल्प वाली तारीख भी पूरी तरह booked है तो उसके बारे में फिर से पूछा जाता है, आँख मूँदकर confirm नहीं किया जाता।

client को क्या मिलता है

client को आपका message मिलता है और उसके साथ model से generate किया गया एक JSON Schema:

{
  "properties": {
    "accept_alternative": {
      "description": "Try another date?",
      "title": "Accept Alternative",
      "type": "boolean"
    },
    "date": {
      "default": "2025-12-26",
      "description": "Alternative date (YYYY-MM-DD)",
      "title": "Date",
      "type": "string"
    }
  },
  "required": ["accept_alternative"],
  "title": "AlternativeDate",
  "type": "object"
}

वही schema form है। Field(description=...) label है; default input को पहले से भर देता है और field को optional बना देता है। यह वही Pydantic-to-JSON-Schema तंत्र है जो Tools tool के arguments के लिए बताता है।

Warning

elicitation schema tool के input schema जितना expressive नहीं होता। सिर्फ़ flat, primitive fields: str, int, float, bool, या strings का Literal (यह enum बन जाता है)। model के अंदर model रखें और ctx.elicit client को कुछ भी भेजे जाने से पहले ही raise कर देता है:

TypeError: Elicitation schema field 'address' rendered as {'$ref': '#/$defs/Address'}, which is not a valid PrimitiveSchemaDefinition

आप किसी इंसान को काम के बीच में टोक रहे हैं। अगर जवाब में nesting चाहिए, तो उसे tool का argument होना चाहिए था।

तीन जवाब

result.action बताता है कि user ने क्या किया, और संभावनाएँ ठीक तीन हैं:

  • "accept": user ने form submit किया। result.data एक AlternativeDate instance है, पहले से validated।
  • "decline": user ने मना कर दिया।
  • "cancel": user ने बिना कुछ चुने सवाल को हटा दिया।

result.data सिर्फ़ "accept" पर ही मौजूद होता है, इसीलिए उदाहरण पहले result.action जाँचता है। आपका type checker यह क्रम लागू करता है: result.action == "accept" के बाद result.data एक AlternativeDate है; उससे पहले .data है ही नहीं।

इनकार कोई error नहीं है। decline का क्या मतलब है, यह tool तय करता है (यहाँ, कोई booking नहीं) और model को सामान्य रूप से जवाब देता है।

Tip

जवाब आपके code तक पहुँचने से पहले आपके model के विरुद्ध validate होता है। जो client bool के लिए "maybe" भेजता है, वह आपकी booking को खराब नहीं करता: call schema-mismatch error के साथ fail हो जाता है, आपका if कभी नहीं चलता।

user को URL पर भेजना

कुछ चीज़ें model या client से होकर कभी नहीं गुज़रनी चाहिए: credentials, card numbers, OAuth consent। इनके लिए आप data नहीं माँगते; आप user से कहीं जाने को कहते हैं:

server.py
from mcp.server import MCPServer
from mcp.server.mcpserver import Context

mcp = MCPServer("Bistro")


@mcp.tool()
async def pay_deposit(booking_id: str, ctx: Context) -> str:
    """Take the deposit that confirms a booking."""
    result = await ctx.elicit_url(
        message="A 20 EUR deposit confirms your booking.",
        url=f"https://pay.example.com/deposit/{booking_id}",
        elicitation_id=f"deposit-{booking_id}",
    )
    if result.action == "accept":
        return "Complete the payment in your browser."
    return "No deposit taken. The booking expires in one hour."


@mcp.tool()
async def confirm_deposit(booking_id: str, ctx: Context) -> str:
    """Record a payment reported by the payment provider."""
    await ctx.session.send_elicit_complete(f"deposit-{booking_id}")
    return f"Deposit received for booking {booking_id}."
  • ctx.elicit_url() message, जाने के लिए URL, और आपकी चुनी हुई एक elicitation_id लेता है: कोई भी string जो आपके server के भीतर इस elicitation को पहचानती हो।
  • result में एक action है और कुछ नहीं। "accept" का मतलब है user URL खोलने के लिए राज़ी हुआ, यह नहीं कि उसने दूसरी तरफ़ का काम पूरा कर लिया।
  • payment out of band होता है, user के browser और आपके payment provider के बीच। MCP से होकर कोई content कभी वापस नहीं आता।

दूसरा tool देखें। जब आपके server को पता चलता है कि out-of-band flow पूरा हो गया (webhook, poll; यहाँ इसे दूसरे tool के रूप में दिखाया गया है), तो ctx.session.send_elicit_complete(...) उसी elicitation_id के साथ notifications/elicitation/complete भेजता है। इसी से client को पता चलता है कि वह "waiting for payment..." दिखाना बंद कर सकता है। इसके बिना client सिर्फ़ अंदाज़ा लगा सकता है।

client की तरफ़

servers पूछते हैं। clients Client(...) को एक elicitation_callback देकर जवाब देते हैं:

client.py
from mcp import Client
from mcp.client import ClientRequestContext
from mcp.types import ElicitRequestParams, ElicitRequestURLParams, ElicitResult


async def handle_elicitation(context: ClientRequestContext, params: ElicitRequestParams) -> ElicitResult:
    if isinstance(params, ElicitRequestURLParams):
        print(f"Open this link to continue: {params.url}")
        return ElicitResult(action="accept")
    print(params.message)
    return ElicitResult(action="accept", content={"accept_alternative": True, "date": "2025-12-27"})


async def main() -> None:
    async with Client(
        "http://127.0.0.1:8000/mcp",
        mode="legacy",
        elicitation_callback=handle_elicitation,
    ) as client:
        result = await client.call_tool("book_table", {"date": "2025-12-25", "party_size": 2})
        print(result.content)
  • एक ही callback दोनों modes संभालता है। params ElicitRequestFormParams और ElicitRequestURLParams का union है; isinstance ही branch है।
  • URL के लिए, आप user को params.url दिखाते हैं और उसका चुना हुआ action लौटाते हैं। कभी कोई content नहीं।
  • form के लिए, असली application params.requested_schema render करता है और user का input content के रूप में लौटाता है। यह वाला हमेशा एक तयशुदा जवाब के साथ हाँ कहता है, जो test में ठीक वैसा ही callback है जैसा आप चाहते हैं।
  • callback देना ही capability declaration भी है: इसी से server को पता चलता है कि इस client से पूछा जा सकता है। client server के लिए और किन चीज़ों का जवाब दे सकता है, वह Client callbacks में है।

Info

elicitation server से client को जाने वाली request है, और ऐसी requests सिर्फ़ classic-handshake session पर ही होती हैं, इसीलिए यह client mode="legacy" देता है। 2026-07-28 connection पर tool इसके बजाय call से सवाल लौटाकर पूछता है; वह flow Multi-round-trip requests है।

इसे आज़माएँ

ctx.elicit वाले form-mode server.py (book_table वाला) को Streamable HTTP पर शुरू करें (one-liner अपना server चलाना में है), फिर client का main() चलाएँ और book_table से Christmas के दिन के लिए पूछें।

callback उसे भेजा गया सवाल print करता है:

No tables for 2 on 2025-12-25. Would you like to try another date?

यह {"accept_alternative": True, "date": "2025-12-27"} से जवाब देता है, और tool, जो इस पूरे समय await ctx.elicit(...) के अंदर इंतज़ार कर रहा था, booking पूरी कर देता है:

Booked a table for 2 on 2025-12-27.

अब URL-mode वाला server.py लगाएँ और उसी main() को pay_deposit की ओर कर दें: वही callback दूसरी branch लेता है, payment link print करता है, और tool "Complete the payment in your browser." के साथ लौटता है। एक round trip, call के बीच में, दोनों दिशाओं में।

Check

अब Client से elicitation_callback= हटाएँ और book_table को Christmas के दिन के लिए फिर से call करें। पूरा call एक protocol error के साथ fail हो जाता है:

Elicitation not supported

जिस client ने कोई callback register नहीं किया, उसने elicitation capability कभी declare ही नहीं की, इसलिए पूछने के लिए कोई है ही नहीं। आपके tool को "decline" नहीं मिला; उसे exception मिला। इसे ध्यान में रखकर design करें: हर elicitation के पास "अगर मैं पूछ न सकूँ तो?" का एक समझदार जवाब होना चाहिए।

सारांश

  • Annotated[T, Resolve(fn)] से annotate किया गया parameter resolver भरता है, जो पूछना ज़रूरी होने पर Elicit(...) लौटाता है। यह हर connection पर काम करता है।
  • schema एक flat Pydantic model है: सिर्फ़ primitive fields, वापसी पर validate होते हैं।
  • result.action "accept", "decline" या "cancel" होता है; result.data सिर्फ़ accept पर मौजूद होता है।
  • await ctx.elicit(message, schema=Model) tool body के अंदर से पूछता है, और await ctx.elicit_url(message, url, elicitation_id) उन सब चीज़ों के लिए है जो model से होकर नहीं गुज़रनी चाहिए (ctx.session.send_elicit_complete(elicitation_id) बताता है कि out-of-band हिस्सा पूरा हो गया)। दोनों server-to-client requests हैं: इन्हें legacy connection वाला client चाहिए।
  • client एक elicitation_callback से जवाब देता है, params के type पर branch करके; उसे register करना ही capability declare करना है।
  • 2026-07-28 connection पर server सवाल को push करने के बजाय लौटाता है; वही callback Multi-round-trip requests से भरता है।

उस return के नीचे जो कुछ भी है (retry loop, requestState की सुरक्षा, इसे खुद चलाना), वह Multi-round-trip requests है।