Middleware
मशीनी अनुवाद
यह page अंग्रेज़ी documentation से अपने-आप अनुवादित किया गया है, और अंग्रेज़ी page ही प्रामाणिक version है। अगर कुछ गलत लगे, तो अनुवाद page बताता है कि इसकी सूचना कैसे दें।
middleware एक async function है जो server को मिलने वाले हर message को wrap करता है।
इसे आप async (ctx, call_next) के रूप में लिखते हैं और server.middleware में append करते हैं। पूरा API बस इतना ही है।
Warning
middleware list source में provisional के रूप में चिह्नित है: इसका signature और semantics किसी 2.x minor release में बदल सकते हैं। इसका इस्तेमाल messages को देखने (timing, logging, tracing) और अस्वीकार करने के लिए करें; इसे वह नींव न बनाएँ जिस पर आपका server खड़ा हो।
MCPServer यह list construction के समय लेता है (MCPServer(name, middleware=[...])) और इसे
mcp.middleware के रूप में उपलब्ध कराता है; low-level Server वही list server.middleware के रूप में देता है। नीचे दिए गए
उदाहरण low-level Server इस्तेमाल करते हैं; अगर Server(name, on_call_tool=...) आपके लिए नया है, तो पहले
Low-level Server पढ़ें।
Timing middleware
एक server, एक tool, एक middleware जो log करता है कि हर message में कितना समय लगा:
import logging
import time
from mcp.server import Server, ServerRequestContext
from mcp.server.context import CallNext, HandlerResult
from mcp.types import (
CallToolRequestParams,
CallToolResult,
ListToolsResult,
PaginatedRequestParams,
TextContent,
Tool,
)
logger = logging.getLogger(__name__)
async def on_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
return ListToolsResult(
tools=[
Tool(
name="search_books",
description="Search the catalog by title or author.",
input_schema={
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
)
]
)
async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
query = (params.arguments or {})["query"]
return CallToolResult(content=[TextContent(type="text", text=f"Found 3 books matching {query!r}.")])
async def log_timing(ctx: ServerRequestContext, call_next: CallNext) -> HandlerResult:
start = time.perf_counter()
try:
return await call_next(ctx)
finally:
elapsed_ms = (time.perf_counter() - start) * 1000
logger.info("%s took %.1f ms", ctx.method, elapsed_ms)
server = Server("Bookshop", on_list_tools=on_list_tools, on_call_tool=on_call_tool)
server.middleware.append(log_timing)
ctxवहीServerRequestContextहै जो आपके handlers को मिलता है।ctx.methodraw method string है;ctx.paramsraw params हैं, किसी भी validation से पहले।call_next(ctx)बाकी chain चलाता है: validation, handler lookup, आपका handler। जो उसने लौटाया वही लौटा दें, तो response जस का तस रहता है।try/finallyजानबूझकर है: जो handler raise करता है उसका समय भी मापा जाता है, क्योंकि failure आपके middleware तकcall_nextसे निकले exception के रूप में पहुँचती है।server.middleware.append(...)इसे register करता है। list outermost-first चलती है, इसलिएmiddleware[0]वह है जो wire के सबसे नज़दीक है।
इसे आज़माएँ
client connect करें, tools की सूची लें, एक को call करें। आपके log में तीन lines हैं:
server/discover took 18.3 ms
tools/list took 0.1 ms
tools/call took 0.1 ms
आपने दो calls किए और तीन lines मिलीं। पहली server/discover है: वह request जो
client ने connection तैयार करने के लिए भेजी, आपके कुछ माँगने से पहले।
यही असली बात है। middleware हर inbound message को wrap करता है:
- connection setup:
server/discover, या legacy session परinitializeऔरnotifications/initialized। - हर request और हर notification जो server तक पहुँचे। notification के लिए
ctx.request_id is Noneहोता है,call_next(ctx)Noneलौटाता है, और आप जो भी लौटाएँ वह फेंक दिया जाता है। (2026-07-28वाले streamable-HTTP path पर client का notification POST transport पर ही202से acknowledge हो जाता है और कभी dispatch नहीं होता, इसलिए वह middleware तक भी नहीं पहुँचता; वह revision HTTP पर कोई client-to-server notifications define ही नहीं करता।) - वह method भी जिसके लिए server के पास कोई handler नहीं है:
call_nextMCPError(-32601, "Method not found")को client की ओर जाते हुए आपके middleware के बीच से raise करता है।
concurrency की सीमा
middleware के लिए call_next(ctx) call करना ज़रूरी नहीं है। इसकी जगह MCPError raise करें और वह एक
message अस्वीकार कर दिया जाता है: connection बना रहता है और अगला message निकल जाता है।
मान लें कि हर search चार connections वाले pool का एक connection थामे रखता है। यह middleware चार tool calls को एक साथ चलने देता है और पाँचवें को अस्वीकार कर देता है:
from typing import Any
from mcp import MCPError
from mcp.server import Server, ServerRequestContext
from mcp.server.context import CallNext, HandlerResult, ServerMiddleware
from mcp.types import (
CallToolRequestParams,
CallToolResult,
ListToolsResult,
PaginatedRequestParams,
TextContent,
Tool,
)
# MCP defines no "busy" error, so this server picks its own code.
SERVER_BUSY = 1
async def on_list_tools(ctx: ServerRequestContext, params: PaginatedRequestParams | None) -> ListToolsResult:
return ListToolsResult(
tools=[
Tool(
name="search_books",
description="Search the catalog by title or author.",
input_schema={
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
},
)
]
)
async def on_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
query = (params.arguments or {})["query"]
return CallToolResult(content=[TextContent(type="text", text=f"Found 3 books matching {query!r}.")])
def max_concurrent_tool_calls(limit: int) -> ServerMiddleware[Any]:
running = 0
async def middleware(ctx: ServerRequestContext, call_next: CallNext) -> HandlerResult:
nonlocal running
if ctx.method != "tools/call":
return await call_next(ctx)
if running >= limit:
raise MCPError(code=SERVER_BUSY, message=f"Server busy: tool call limit reached ({limit} in progress).")
running += 1
try:
return await call_next(ctx)
finally:
running -= 1
return middleware
server = Server("Bookshop", on_list_tools=on_list_tools, on_call_tool=on_call_tool)
server.middleware.append(max_concurrent_tool_calls(4))
- सिर्फ़
tools/callगिना जाता है, इसलिए tool calls अस्वीकार करते समय भी serverserver/discoverऔरtools/listका जवाब देता रहता है। - MCP कोई "server busy" error code define नहीं करता, इसलिए
SERVER_BUSYइस server का अपना है। - अस्वीकार करने से client को तुरंत पता चल जाता है कि server overloaded है। अगर आप callers को इंतज़ार कराना
बेहतर समझते हैं, तो इसकी जगह
call_next(ctx)के चारों ओरanyio.CapacityLimiterhold करें।
raise किया गया MCPError client application को जाता है, model को नहीं। अगर message model को पढ़ना चाहिए,
तो इसकी जगह is_error=True वाला tool result लौटाएँ: यही नीचे वाला जवाब दें है।
इसके अंदर आप क्या कर सकते हैं
इस क्रम में कि आपको कितना हिचकना चाहिए, कम से ज़्यादा की ओर:
- देखें। समय मापें, गिनें, log करें। ऊपर वाला timing middleware।
- अस्वीकार करें।
call_next(ctx)call करने के बजायMCPErrorraise करें और उस एक message का जवाब JSON-RPC error से दिया जाता है। connection बना रहता है; अगला message निकल जाता है। ऊपर वाली concurrency की सीमा। server हर caller के लिएsubscriptions/listenको भी इसी तरह gate करता है: Subscriptions page पर यह तय करना कि कौन देख सकता है इसे चरण दर चरण समझाता है। - फिर से लिखें।
ctxdataclass है:await call_next(dataclasses.replace(ctx, params=...))बाकी chain को client के भेजे params से अलग params देता है।initializeके साथ ऐसा कभी न करें: client को जो result वापस मिलता है वह आपके बदले हुए params से बनता है, लेकिन server अपनी connection state मूल wire params से commit करता है। दोनों पक्ष handshake इस असहमति के साथ पूरा कर सकते हैं कि उन्होंने क्या negotiate किया। - जवाब दें।
call_next(ctx)call किए बिना result लौटाएँ और वह आपके response के रूप में client को जाता है।call_nextआपको तैयार wire form देता है, और pipeline आप जो लौटाते हैं उसे कभी patch नहीं करता, इसलिए पूरा envelope आपका है: 2026 पीढ़ी के connection पर इसमेंserverInfoका_metastamp शामिल है, जिसे SDK handler results में जोड़ता है पर आपके results में नहीं।
Check
initialize उन चीज़ों में से एक है जिन्हें middleware wrap करता है, और इसके लिए आपको मिलने वाला यह एकमात्र hook है।
add_request_handler से इसे अपने हाथ में लेने की कोशिश करें तो SDK मना कर देता है:
ValueError: 'initialize' is handled by the server runner and cannot be overridden;
use Server.middleware to observe or wrap initialization
Warning
initialize inline संभाला जाता है: जब तक आपकी middleware chain लौट नहीं आती, server आगे कोई inbound
message नहीं पढ़ता। इसलिए initialize संभालते समय server-to-client request (ctx.session.send_request(...),
कोई elicitation) को await करना connection को deadlock कर देता है: जिस
response का आप इंतज़ार कर रहे हैं वह कभी पढ़ा ही नहीं जा सकता। fire-and-forget notifications ठीक हैं।
वह एक middleware जो default रूप से चालू आता है
SDK ठीक एक middleware साथ देता है, और वह पहले से आपके server की list में है: वह जो हर message के लिए OpenTelemetry span emit करता है। आप इसे append नहीं करते, और ज़्यादातर समय इसके बारे में सोचते भी नहीं। जब तक आप कोई exporter install नहीं करते यह no-op है, और इसका अपना page है: OpenTelemetry।
Info
अगर आपने ASGI middleware लिखा है, तो यह आकार आप पहले से जानते हैं। Starlette का
(scope, receive, send) यहाँ (ctx, call_next) बन गया, और यह transport के बाद चलता है,
raw HTTP request की जगह decoded message पर। दोनों साथ मिलकर काम करते हैं: streamable_http_app() पर
Starlette middleware HTTP देखता है; यह MCP देखता है।
सारांश
- middleware
async (ctx, call_next) -> resultहै, जिसेMCPServer(middleware=[...])के रूप में पास किया जाता है (याmcp.middlewareमें append किया जाता है), और low-levelServerपरserver.middlewareमें append किया जाता है। - यह server तक पहुँचने वाले हर inbound message को wrap करता है (
server/discover,initialize, requests, notifications, अनजान methods) और outermost-first चलता है। ctx.request_id is Noneसे आप notification और request में फ़र्क करते हैं।- एक message को अस्वीकार करने के लिए
call_nextcall करने के बजाय raise करें; connection बचा रहता है। - SDK का अपना OpenTelemetry tracing भी एक middleware है, जो पहले से list में है। देखें OpenTelemetry।
- पूरा surface provisional है। इससे देखें; इस पर निर्माण न करें।
request को wrap करने वाली हर चीज़ बस इतनी ही है। Authorization वह है जो तय करता है कि request को चलने दिया जाए भी या नहीं।