MCP Tasks Extension: Async और बैकग्राउंड टास्क को समझें
लंबे टूल कॉल टाइम आउट हो जाते हैं, कनेक्शन टूटते हैं और काम खो जाता है। MCP Tasks एक्सटेंशन एक टिकाऊ taskId, tasks/get पोलिंग, input_required पॉज़ और कोऑपरेटिव कैंसल के ज़रिए इसे ठीक करता है। लाइफ़साइकल, JSON payloads, FastMCP सर्वर का उदाहरण और ऐसी क्लाइंट आदतें देखें जो क्रैश के बाद भी सुरक्षित रहें।
आपका एजेंट एक टूल कॉल करता है, टूल को चालीस मिनट चाहिए, और दूसरे मिनट के आसपास कोई प्रॉक्सी कनेक्शन बंद कर देती है। जॉब शायद सर्वर पर अब भी चल रहा हो, पर अब कोई उस तक पहुँच नहीं सकता, और मॉडल के हाथ में जवाब की जगह एक एरर रह जाता है। यही खाई MCP Tasks एक्सटेंशन को बंद करने के लिए बनाई गई है। काम खत्म होने तक एक request खुला रखने की बजाय सर्वर तुरंत एक टिकाऊ taskId लौटाता है, और क्लाइंट जब चाहे तब जाँच कर सकता है।
यह लेख दिखाता है कि Model Context Protocol में async और बैकग्राउंड टास्क कैसे काम करते हैं: एक्सटेंशन क्या है, हर स्टेटस का क्या मतलब है, payloads कैसे दिखते हैं, टास्क-कैपेबल सर्वर कैसे बनता है, और कौन सी क्लाइंट आदतें लंबे जॉब्स को सुरक्षित रखती हैं। नीचे दिए फ़ील्ड नाम प्रकाशित एक्सटेंशन स्पेक (io.modelcontextprotocol/tasks, SEP-2663) और FastMCP डॉक्यूमेंटेशन से लिए गए हैं।
ब्लॉकिंग कॉल क्यों टूटती हैं
एक स्टैंडर्ड MCP tools/call उस ग्राहक की तरह व्यवहार करता है जो काउंटर पर खड़ा होकर डिश का इंतज़ार करता है। रिक्वेस्ट जाती है, कनेक्शन खुला रहता है, और जवाब उसी लाइन पर लौट आता है। मौसम की जानकारी के लिए यह बिल्कुल ठीक है। CI पाइपलाइन, बल्क इम्पोर्ट या मॉडल ट्रेनिंग रन के लिए यह एक बुरा सौदा है।
एक रेस्टोरेंट इसे अलग तरीके से हल करता है। कोई भी किचन पास पर खड़े होकर शेफ़ को नहीं घूरता। वेटर एक पेपर टिकट रेल पर क्लिप कर देता है, और वही टिकट ऑर्डर का हैंडल है। Tasks MCP को वही टिकट रेल देते हैं।
टाइमआउट की समस्या
कई क्लाइंट और ट्रांसपोर्ट इंटरमीडियरी टाइमआउट लगाते हैं, जिससे कुछ सेकंड से ज़्यादा देर तक request खुली रखना व्यावहारिक नहीं रहता। लोड बैलेंसर, कॉरपोरेट प्रॉक्सी और सर्वरलेस गेटवे, सभी निष्क्रिय (idle) कनेक्शन काट देते हैं। जब ऐसा होता है, तो caller को विफलता दिखती है, जबकि सर्वर अभी भी व्यस्त होता है, और स्वाभाविक प्रतिक्रिया होती है वही महँगा जॉब दोबारा शुरू कर देना।
डिस्कनेक्ट के बाद खोया काम
ब्लॉकिंग कॉल नतीजे को कनेक्शन से बाँध देती है। लैपटॉप का ढक्कन बंद होता है, मोबाइल क्लाइंट wifi से बाहर चला जाता है, या होस्ट प्रोसेस रीस्टार्ट होता है, और जवाब पहुँचने की कोई जगह नहीं बचती। टास्क के साथ ID एक टिकाऊ हैंडल होती है: क्लाइंट दोबारा कनेक्ट होता है, उसी ID के साथ tasks/get कॉल करता है, और ठीक वहीं से आगे बढ़ता है जहाँ रुका था।
💡 मोटा नियम: अगर कोई ऑपरेशन अक्सर कुछ सेकंड से ज़्यादा लेता है, या किसी इंसानी फ़ैसले के लिए रुकता है, तो उसे टास्क में लपेटना चाहिए।
Tasks एक्सटेंशन क्या जोड़ता है
कोर स्पेक से एक्सटेंशन तक
Tasks की शुरुआत MCP कोर स्पेसिफ़िकेशन में एक प्रायोगिक फ़ीचर के रूप में हुई। तब से प्रोटोकॉल ने इन्हें कोर से निकालकर io.modelcontextprotocol/tasks पहचान वाले एक वैकल्पिक एक्सटेंशन में डाल दिया है, जिसका विवरण SEP-2663 में है। कोर के बाहर रहने से बेस प्रोटोकॉल छोटा रहता है, और जिन सर्वर और क्लाइंट को लंबे समय चलने वाले काम चाहिए, वे जानबूझकर opt in करते हैं। आधिकारिक डॉक्स इसे लंबे MCP ऑपरेशनों के लिए असिंक्रोनस टास्क एक्ज़ीक्यूशन बताते हैं, और पूरी स्पेसिफ़िकेशन ext-tasks रिपॉज़िटरी में है।
एक बदलाव ध्यान देने लायक है। फ़ीचर के पुराने विवरणों में एक अलग tasks/result कॉल का ज़िक्र है। एक्सटेंशन में अंतिम आउटपुट tasks/get रिस्पॉन्स के भीतर आता है, जिससे क्लाइंट का लूप सिर्फ़ एक पोलिंग मेथड तक सीमित रहता है।
एक्सटेंशन आइडेंटिफ़ायर और opt in
समर्थन पर बातचीत होती है, उसे कभी मान नहीं लिया जाता:
क्लाइंटio.modelcontextprotocol/tasks को अपनी per-request capabilities में सूचीबद्ध करता है, _meta के भीतर io.modelcontextprotocol/clientCapabilities के तहत।
सर्वर वही एक्सटेंशन उन capabilities में विज्ञापित करता है जो वह क्लाइंट्स को लौटाता है।
अगर सर्वर को टास्क सपोर्ट चाहिए और क्लाइंट ने उसे कभी घोषित नहीं किया, तो सर्वर एरर कोड -32003 और संदेश Missing required client capability के साथ जवाब देता है।
टास्क कब बनाना है, यह फ़ैसला सर्वर का होता है। क्लाइंट की तरफ़ कोई प्रति-टूल फ़्लैग नहीं होता। क्लाइंट एक बार opt in करता है और दो रिज़ल्ट शेप के लिए तैयार रहना चाहिए: सामान्य रिज़ल्ट, या टास्क हैंडल। आज tools/call ही एकमात्र रिक्वेस्ट टाइप है जो टास्क बना सकता है।
साइड
उसे क्या करना होता है
यह क्यों मायने रखता है
क्लाइंट
एक्सटेंशन घोषित करे, दो रिज़ल्ट शेप संभाले
सर्वर किसी ऐसे क्लाइंट को टास्क नहीं लौटाता जिसने opt in नहीं किया
सर्वर
एक्सटेंशन विज्ञापित करे, जवाब देने से पहले टास्क बनाए
जवाब भेजने के ठीक बाद हुआ क्रैश ID को अनाथ नहीं बना सकता
दोनों
taskId को ही एकमात्र हैंडल मानें
दोबारा कनेक्शन और रीस्टार्ट नुकसानदेह नहीं रहते
टास्क लाइफ़साइकल
हैंडल लेना और पोल करना
फ़्लो में पाँच चरण हैं:
क्लाइंट tasks capability जोड़कर tools/call भेजता है।
सर्वर तय करता है कि काम लंबा है और resultType: "task" चिह्नित CreateTaskResult लौटाता है।
सर्वर से वह जवाब निकलने से पहले टास्क टिकाऊ तरीके से बन चुका होता है।
क्लाइंट taskId के साथ tasks/get कॉल करता है, कॉल्स के बीच कम से कम pollIntervalMs इंतज़ार करते हुए।
हर रिस्पॉन्स में मौजूदा स्टेटस होता है, और टास्क टर्मिनल होने पर उसमें नतीजा या एरर होता है।
यह एक नए टास्क का सरल दृश्य है। सटीक envelope स्पेसिफ़िकेशन में परिभाषित है, इसलिए इसे फ़ील्ड्स का उदाहरण समझें:
सर्वर को क्लाइंट का इनपुट चाहिए, देखें inputRequests
नहीं
completed
ऑपरेशन पूरा हुआ, result फ़ील्ड में आउटपुट है
हाँ
failed
JSON-RPC एरर हुआ, error फ़ील्ड में विवरण है
हाँ
cancelled
अनुरोध पर रोका गया, हालाँकि सर्वर इसका हमेशा पालन नहीं करता
हाँ
एक बार टास्क टर्मिनल स्टेटस पर पहुँच जाए, तो उसकी स्थिति फिर कभी नहीं बदलती। tasks/get idempotent है, इसलिए दस बार पोल करना उतना ही सुरक्षित है जितना एक बार।
इंसानी इनपुट के लिए रुकना
कुछ जॉब बीच में एक फ़ैसले के बिंदु पर पहुँचते हैं: डिप्लॉय को मंज़ूरी देना, खरीद की पुष्टि करना, तीन विकल्पों में से एक चुनना। टास्क input_required में चला जाता है, और अगला tasks/get रिस्पॉन्स एक inputRequests मैप शामिल करता है जिसमें elicitations या दूसरे सर्वर रिक्वेस्ट होते हैं।
क्लाइंट ये रिक्वेस्ट किसी यूज़र या मॉडल को दिखाता है, फिर tasks/update से जवाब देता है, और inputResponses भेजता है जो बकाया रिक्वेस्ट्स से मेल खाते हों। सर्वर एक खाली रिज़ल्ट के साथ पावती देता है और अज्ञात या पहले से पूरी हो चुकी एंट्री के जवाब अनदेखे कर देता है। हर रिक्वेस्ट का जवाब आने के बाद सर्वर आगे बढ़ता है।
💡 यह क्यों बढ़िया है: न दूसरा कनेक्शन चाहिए, न सर्वर-से-क्लाइंट का बिना माँगा मैसेज। इंसानी कदम भी उसी पोलिंग लूप पर चलता है जिस पर बाकी सब चलता है।
पूरा होना, फ़ेल होना और रद्द करना
जब टास्क सफलतापूर्वक पूरा होता है, तो result फ़ील्ड में वही होता है जो मूल रिक्वेस्ट सिंक्रोनस रूप से लौटाती। टूल कॉल के लिए इसका मतलब है वही कंटेंट ब्लॉक जो ब्लॉकिंग कॉल बनाती। जब स्टेटस failed हो, तब error फ़ील्ड में JSON-RPC एरर होता है।
रद्द करने के लिए tasks/cancel का इस्तेमाल होता है। सर्वर एक खाली रिज़ल्ट के साथ पावती देता है, लेकिन रद्द करना कोऑपरेटिव है। काम शायद पहले ही उस बिंदु से आगे जा चुका हो जहाँ से लौटना संभव नहीं, इसलिए टास्क अब भी किसी अलग टर्मिनल स्टेटस पर पहुँच सकता है।
सर्वर notifications/tasks के ज़रिए अपडेट भी भेज सकते हैं। क्लाइंट subscriptions/listen से opt in करते हैं, और हर नोटिफ़िकेशन में पूरा टास्क स्टेट होता है, वही शेप जो tasks/get रिस्पॉन्स लौटाता।
टास्क सर्वर बनाना
एक न्यूनतम FastMCP टूल
FastMCP 4.0 ने इस एक्सटेंशन का समर्थन जोड़ा। आप fastmcp-tasks इंस्टॉल करते हैं, TasksExtension रजिस्टर करते हैं, और टूल को टास्क-कैपेबल चिह्नित करते हैं:
import asyncio
from fastmcp import FastMCP
from fastmcp_tasks import TasksExtension
mcp = FastMCP("ReportServer")
mcp.add_extension(TasksExtension())
@mcp.tool(task=True)
async def slow_computation(duration: int) -> str:
"""A long-running operation."""
for i in range(duration):
await asyncio.sleep(1)
return f"Finished in {duration} seconds"
यहाँ दो बातें मायने रखती हैं। बैकग्राउंड टास्क के लिए async फ़ंक्शन ज़रूरी हैं, और sync फ़ंक्शन पर task=True लगाने से रजिस्ट्रेशन के समय ValueError उठता है। और task=True सिर्फ़ यह संकेत देता है कि टूल बैकग्राउंड में चल सकता है। वह असल में चलता है या नहीं, यह इस पर निर्भर करता है कि क्लाइंट opt in करता है या नहीं, और सर्वर का एक्ज़ीक्यूशन मोड क्या है। डॉक्स बताते हैं कि Docket वितरित शेड्यूलर का आधार है, और यही सेटअप को प्रोडक्शन के लिए तैयार बनाता है।
प्रोग्रेस और एक्ज़ीक्यूशन मोड
टूल इंजेक्टेड Progress डिपेंडेंसी के ज़रिए प्रोग्रेस रिपोर्ट करते हैं, और वहीं से वह statusMessage आता है जो आपके क्लाइंट दिखाते हैं:
ज़्यादा नियंत्रण के लिए बूलियन की जगह TaskConfig लगाएँ। तीन मोड तय करते हैं कि टूल कैसे व्यवहार करेगा:
मोड
व्यवहार
optional
लीगेसी क्लाइंट्स के लिए सिंक्रोनस चलता है, और टास्क-कैपेबल क्लाइंट्स के लिए बैकग्राउंड में
required
क्लाइंट में टास्क सपोर्ट न हो तो एरर देता है, वरना बैकग्राउंड में चलता है
forbidden
हमेशा सिंक्रोनस, कभी बैकग्राउंड में नहीं
शॉर्टकट साफ़ तौर पर मैप होते हैं: task=True का मतलब optional है, और task=False का मतलब forbidden है। आप poll_interval=timedelta(seconds=2) के साथ पोलिंग की गति भी सुझा सकते हैं।
टिकाऊ क्लाइंट पैटर्न
विनम्रता से पोल करें, सब कुछ सहेजें
टास्क-कैपेबल सर्वरों से बात करने वाले क्लाइंट को पाँच आदतों की ज़रूरत है:
Extension घोषित करें per-request capabilities में।
Polymorphic रिज़ल्ट संभालें, क्योंकि tools/call सामान्य रिज़ल्ट या टास्क लौटा सकता है।
pollIntervalMs का सम्मान करें, क्योंकि सर्वर रिस्पॉन्स के बीच उसे बदल सकता है।
inputRequests का जवाब देंtasks/update के ज़रिए, उन्हें अनदेखा न करें।
टास्क ID टिकाऊ तरीके से स्टोर करें ताकि क्रैश या रीस्टार्ट के बाद पोलिंग फिर शुरू हो सके।
नीचे दिया लूप pseudocode है, किसी खास SDK से बँधा नहीं:
async def run_tool(session, name, args):
reply = await session.call_tool(name, args)
if reply.get("resultType") != "task":
return reply # ordinary synchronous result
task = reply
store.save(task["taskId"]) # survive a crash
while task["status"] in ("working", "input_required"):
if task["status"] == "input_required":
answers = await ask_user(task["inputRequests"])
await session.request("tasks/update", {
"taskId": task["taskId"],
"inputResponses": answers,
})
await asyncio.sleep(task["pollIntervalMs"] / 1000)
task = await session.request("tasks/get", {"taskId": task["taskId"]})
if task["status"] == "failed":
raise RuntimeError(task["error"])
return task.get("result")
ट्रेन पर बैठा यात्री इसके लिए सही मानसिक मॉडल है। हर सुरंग में कनेक्शन टूटता है, फिर भी जेब में रखा टिकट वैध रहता है। इस तरह बना क्लाइंट सुरंग के बाद दोबारा कनेक्ट होता है और आगे बढ़ता रहता है।
पोलिंग की जगह नोटिफ़िकेशन
पोलिंग डिफ़ॉल्ट है, और वह हर जगह काम करती है। अगर सर्वर notifications/tasks सपोर्ट करता है, तो क्लाइंट एक बार सब्सक्राइब कर सकता है और ज़्यादातर tasks/get राउंड-ट्रिप छोड़ सकता है, क्योंकि हर नोटिफ़िकेशन में पहले से पूरा टास्क स्टेट होता है। जो सर्वर पुश नहीं करते, उनके लिए पोलिंग को फ़ॉलबैक के रूप में रखें।
किन गलतियों से बचें
टास्क हैंडल पोस्ट ऑफ़िस के पिछले कमरे के पार्सलों जैसे होते हैं। किसी को बहुत देर पड़ा रहने दें, तो वह साफ़ कर दिया जाता है। ये वे जाल हैं जो सबसे ज़्यादा दिखते हैं:
गलती
क्या गड़बड़ होती है
समाधान
ttlMs को नज़रअंदाज़ करना
धीमे क्लाइंट के नतीजा पढ़ने से पहले टास्क एक्सपायर हो जाता है
नतीजे तुरंत पढ़ें, और सर्वर को ऐसा TTL दें जो असली क्लाइंट व्यवहार में फ़िट बैठे
pollIntervalMs से तेज़ पोल करना
बेकार रिक्वेस्ट्स और टाले जा सकने वाला लोड
सुझाए गए अंतराल तक रुकें
रद्द करने को तुरंत मानना
UI दावा करता है कि जॉब रुक गया, जबकि वह चलता रहता है
उसकी रिपोर्ट करने से पहले टर्मिनल स्टेटस का इंतज़ार करें
ऐसे क्लाइंट को टास्क लौटाना जिसने opt in नहीं किया
क्लाइंट रिस्पॉन्स नहीं पढ़ पाता
पहले घोषित capabilities जाँचें
हर टूल को टास्क में लपेटना
तेज़ कॉल्स बिना वजह लेटेंसी झेलती हैं
तेज़ ऑपरेशन को सामान्य रूप से लौटने दें
टास्क ID को ढीले तरीके से साझा करना
कोई दूसरा caller किसी और का आउटपुट पढ़ सकता है
ID को हैंडल मानें और उसे प्रमाणित caller से बाँधें (अच्छा अभ्यास है, स्पेक में जो सूचीबद्ध है उससे आगे)
ttlMs का null मतलब असीमित है, जो सुनने में अच्छा लगता है, जब तक स्टोरेज ऐसे पूरे हुए जॉब्स से न भर जाए जिन्हें कोई उठाता नहीं।
टास्क को PicassoIA के साथ जोड़ना
जनरेटिव मीडिया लंबे चलने वाले जॉब का क्लासिक उदाहरण है, इसलिए इमेज और वीडियो टूलिंग के साथ टास्क पैटर्न स्वाभाविक लगते हैं। PicassoIA के दो मॉडल सीधे टास्क वर्कफ़्लो में फिट बैठते हैं।
Claude Sonnet 5 मल्टी-स्टेप कोडिंग और टूल-यूज़ वाला काम सँभालता है, इसलिए इस लेख के हैंडलर कोड के लिए यह एक भरोसेमंद पेयर प्रोग्रामर है। उसी श्रेणी के दूसरे विकल्पों में GPT 5.6 Sol भी है, अगर आप उसी कोड पर दूसरी राय चाहते हैं।
अपना टूल सिग्नेचर और इस लेख के टास्क फ़ील्ड्स Prompt बॉक्स में पेस्ट करें, फिर स्टेटस मैसेज के साथ async हैंडलर माँगें।
जटिल स्टेट मशीन के लिए Effort को high पर सेट करें, या छोटे संपादनों के लिए low पर छोड़ दें।
System Prompt जोड़ें, जैसे "Write Python, async only, no blocking calls", ताकि हर जवाब एक ही स्टाइल में रहे।
अगर उसी जवाब में टेस्ट चाहिए, तो Max Tokens को 8,192 के डिफ़ॉल्ट से ऊपर बढ़ाएँ।
चलाएँ, नतीजा पढ़ें, और हैंडलर को अपने प्रोजेक्ट में पेस्ट करें।
💡 टिप:Image फ़ील्ड से किसी एरर का स्क्रीनशॉट अटैच करें। मॉडल उसे संदर्भ के रूप में पढ़ता है।
डायग्राम के लिए साफ़ कटआउट
टास्क फ़्लो के बारे में डॉक्यूमेंटेशन को साफ़ विज़ुअल्स चाहिए होते हैं: स्टेटस कार्ड के लिए डिवाइस की फ़ोटो, आर्किटेक्चर स्लाइड के लिए लोगो। बैकग्राउंड हटाना कुछ ही सेकंड में पारदर्शी PNG लौटाता है, और इसकी Preserve Partial Alpha सेटिंग किनारों को प्राकृतिक नरम रखती है। प्रोडक्ट शॉट्स के लिए कठोर, पूरी तरह अपारदर्शी किनारे चाहिए तो इसे बंद करें।
नए दृश्य वाली इमेज के लिए, Flux 2 Pro और P Image दोनों लिखे प्रॉम्प्ट को ऐसी फ़ोटो में बदल देते हैं जो ब्लॉग हेडर के लिए ठीक बैठती है।
अगली बार अपनी इमेज बनाएँ
अब आपके पास पूरी तस्वीर है: रोके गए कनेक्शन की जगह एक हैंडल, पाँच स्टेटस, तीन मेथड्स, और आदतों की एक छोटी सूची जो लंबे जॉब्स को गायब होने से बचाती है। इसे अपनाने का सबसे अच्छा तरीका है कुछ छोटा बनाना। ऐसा टूल लिखें जिसे दस सेकंड लगें, उसे task=True चिह्नित करें, और देखें कि स्टेटस working से किसी टर्मिनल स्टेटस तक कैसे बदलता है।
फिर अपने प्रोजेक्ट को एक दृश्य पहचान दें। Picasso IA खोलें, कोई टेक्स्ट-टू-इमेज मॉडल चुनें, और अपने लेख के लिए हेडर इमेज बनाएँ। अपने प्रॉम्प्ट में कैमरा एंगल, रोशनी और लेंस के विवरण आज़माएँ, साफ़ लोगो के लिए बैकग्राउंड हटाएँ, और देखें कि एक विचार कितनी जल्दी पूरी तरह तैयार विज़ुअल में बदल जाता है। आपके अगले टास्क के नतीजे को ऐसी तस्वीर चाहिए जो साझा करने लायक हो।