MCP Apps
मशीनी अनुवाद
यह page अंग्रेज़ी documentation से अपने-आप अनुवादित किया गया है, और अंग्रेज़ी page ही प्रामाणिक version है। अगर कुछ गलत लगे, तो अनुवाद page बताता है कि इसकी सूचना कैसे दें।
MCP App ऐसा tool है जिसका एक चेहरा है: अपने data के साथ-साथ tool एक HTML document की ओर इशारा करता है, जिसे host interactive surface के रूप में render करता है।
दो हिस्से, हमेशा दो हिस्से:
- एक tool, जो काम करता है और data लौटाता है, किसी भी दूसरे tool की तरह।
- एक
ui://resource, जिसमें वह HTML है जो host उसके लिए दिखाता है।
tool में resource का _meta.ui.resourceUri reference होता है। host उसे resources/read से fetch करता है, sandboxed iframe में render करता है, और tool का result postMessage के ज़रिए उस iframe में भेजता है। आपका server कभी कोई ui/* message न भेजता है, न पाता है: वह traffic host और iframe के बीच का है। आप एक tool और एक HTML document serve करते हैं; दिखाने का सारा काम host करता है।
SDK इसे built-in Apps extension (io.modelcontextprotocol/ui) के रूप में देता है। अगर Extensions आपके लिए नए हैं, तो पहले उस page पर एक नज़र डाल लें। एक मिनट लगेगा, फिर वापस आएँ।
चेहरे वाली घड़ी
from mcp import Client
from mcp.client import advertise
from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID, Apps, client_supports_apps
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.context import Context
CLOCK_HTML = """\
<!doctype html>
<title>Clock</title>
<h1 id="now">...</h1>
<script>
window.addEventListener("message", (event) => {
const text = event.data?.result?.content?.[0]?.text;
if (text) document.getElementById("now").textContent = text;
});
</script>
"""
apps = Apps()
@apps.tool(resource_uri="ui://clock/app.html", description="The current time.")
def get_time(ctx: Context) -> str:
now = "2026-06-26T12:00:00Z"
if not client_supports_apps(ctx):
return f"The time is {now}."
return now
apps.add_html_resource("ui://clock/app.html", CLOCK_HTML, title="Clock")
mcp = MCPServer("clock", extensions=[apps])
async def main() -> None:
async with Client(mcp, extensions=[advertise(EXTENSION_ID, {"mimeTypes": [APP_MIME_TYPE]})]) as client:
result = await client.call_tool("get_time", {})
print(result.content)
# [TextContent(text='2026-06-26T12:00:00Z')]
चार कदम:
Apps(): एक instance में आपके UI-bound tools और उनके resources रहते हैं।@apps.tool(resource_uri="ui://clock/app.html"): एक साधारण tool, साथ में_meta.ui.resourceUriकी मुहर। जो कुछ@mcp.tool()लेता है (name, title, description, ...) वह सब यहाँ भी चलता है।apps.add_html_resource("ui://clock/app.html", CLOCK_HTML): उससे मेल खाता resource, जोtext/html;profile=mcp-appके रूप में serve होता है। ठीक यही MIME type host को बताता है "यह app है, इसे render करें"।MCPServer("clock", extensions=[apps]): opt in करें। server अबcapabilities.extensionsके तहतio.modelcontextprotocol/uiadvertise करता है।
HTML खुद host के postMessage को सुनता है और result दिखाता है। असली
apps के लिए अपने HTML के अंदर official @modelcontextprotocol/ext-apps
browser SDK इस्तेमाल करें। यह आपको raw message events की जगह ontoolresult, callServerTool,
getHostContext और onhostcontextchanged देता है।
Graceful degradation
हर client apps render नहीं करता। इसका आपके लिए क्या मतलब है, spec साफ़-साफ़ कहता है:
UI उपलब्ध होने पर भी tools का एक सार्थक
contentarray लौटाना अनिवार्य (MUST) है।
model content पढ़ता है; iframe इंसानों के लिए है। UI-capable host भी text result
model को देता है, और text-only client को सिर्फ़ वही मिलता है। इसलिए मानक
pattern है: एक tool, दो जवाब। get_time को फिर से देखें:
from mcp import Client
from mcp.client import advertise
from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID, Apps, client_supports_apps
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.context import Context
CLOCK_HTML = """\
<!doctype html>
<title>Clock</title>
<h1 id="now">...</h1>
<script>
window.addEventListener("message", (event) => {
const text = event.data?.result?.content?.[0]?.text;
if (text) document.getElementById("now").textContent = text;
});
</script>
"""
apps = Apps()
@apps.tool(resource_uri="ui://clock/app.html", description="The current time.")
def get_time(ctx: Context) -> str:
now = "2026-06-26T12:00:00Z"
if not client_supports_apps(ctx):
return f"The time is {now}."
return now
apps.add_html_resource("ui://clock/app.html", CLOCK_HTML, title="Clock")
mcp = MCPServer("clock", extensions=[apps])
async def main() -> None:
async with Client(mcp, extensions=[advertise(EXTENSION_ID, {"mimeTypes": [APP_MIME_TYPE]})]) as client:
result = await client.call_tool("get_time", {})
print(result.content)
# [TextContent(text='2026-06-26T12:00:00Z')]
client_supports_apps(ctx) तभी True होता है जब client ने
io.modelcontextprotocol/ui extension declare किया हो और अपनी mimeTypes
settings में text/html;profile=mcp-app सूचीबद्ध किया हो। यह field ज़रूरी है,
इसलिए जो client इसे छोड़ देता है वह गिना नहीं जाता। इसी file में main() ठीक यही
declare करता है: negotiation का client वाला आधा हिस्सा, और rich जवाब वापस आता है।
Warning
कभी भी "[Rendered UI]" जैसा placeholder अकेले content के रूप में न लौटाएँ।
अगर fallback text बेकार है, तो tool हर text-only client के लिए और खुद model
के लिए बेकार है। वह वाक्य लिखें।
iframe को lock करना
सुरक्षा metadata resource वाले हिस्से पर रहता है: iframe क्या load कर सकता है, उसे कौन-सी browser permissions चाहिए, वह किस तरह frame होना चाहेगा:
from mcp.server.apps import Apps, ResourceCsp, ResourcePermissions
from mcp.server.mcpserver import MCPServer
DASHBOARD_HTML = "<!doctype html><title>Dashboard</title><canvas id='chart'></canvas>"
apps = Apps()
@apps.tool(resource_uri="ui://dashboard/app.html", visibility=["app"])
def refresh_dashboard() -> str:
"""Refresh the dashboard data."""
return "refreshed"
apps.add_html_resource(
"ui://dashboard/app.html",
DASHBOARD_HTML,
title="Dashboard",
csp=ResourceCsp(connect_domains=["https://api.example.com"]),
permissions=ResourcePermissions(clipboard_write={}),
domain="dashboard.example.com",
prefers_border=True,
)
mcp = MCPServer("dashboard", extensions=[apps])
csp और permissions host से की गई requests हैं, server का व्यवहार नहीं। host
इन्हीं से iframe की Content-Security-Policy और Permissions-Policy बनाता है, और
मना भी कर सकता है। अनुमति मिल ही गई, यह मानने के बजाय अपने JS में feature-detect करें।
ResourceCsp, एक-एक field करके (Python नाम, wire key, host उसके साथ क्या करता है):
| Python | Wire (_meta.ui.csp) |
क्या नियंत्रित करता है |
|---|---|---|
connect_domains |
connectDomains |
connect-src: fetch/XHR कहाँ जा सकते हैं |
resource_domains |
resourceDomains |
img-src, style-src, ...: static assets |
frame_domains |
frameDomains |
frame-src: nested iframes |
base_uri_domains |
baseUriDomains |
base-uri: <base> किस ओर इशारा कर सकता है |
ResourcePermissions: हर field iframe के लिए एक browser permission माँगता है।
| Python | Wire (_meta.ui.permissions) |
|---|---|
camera |
camera |
microphone |
microphone |
geolocation |
geolocation |
clipboard_write |
clipboardWrite |
Note
CSP और permissions resource पर रहते हैं, tool पर कभी नहीं। spec के tool
metadata में इनके लिए कोई जगह नहीं है, और hosts वहाँ इन्हें अनदेखा करते हैं। SDK इस
गलती को लिखना ही नामुमकिन बना देता है: @apps.tool() में csp parameter है ही नहीं।
Visibility
tool पर visibility=["app"] कहता है "यह iframe के लिए है, model के लिए नहीं":
"model": model इसे call कर सकता है।"app": iframe इसे call कर सकता है (callServerToolके ज़रिए)।- छोड़ दिया जाए: दोनों, जो default है।
Filtering host का काम है। आपका server app-only tools को tools/list में किसी भी
दूसरे tool की तरह सूचीबद्ध करता है; host उन्हें model से छिपाता है। server-side filter न करें।
वे नियम जो SDK लागू करता है
ये सब startup पर ही fail होते हैं, production में नहीं:
- जो
resource_uriया resource URIui://...नहीं है, वह decoration/registration के समयValueErrorहै। - ऐसे URI से बँधा tool जिसका कोई मेल खाता registered resource नहीं है, तब
ValueErrorहै जबMCPServer(extensions=[apps])extension को consume करता है। ऐसा tool जो HTML advertise करे परresources/readपर 404 दे, misconfiguration है, इसलिए server construct होने से मना कर देता है। @apps.tool()परmeta={"ui": ...}ValueErrorहै।_meta["ui"]decorator का है; अपनी बातresource_uri=औरvisibility=से कहें। बाकीmeta=keys साथ में आराम से merge हो जाती हैं।
आज न TypeScript ext-apps SDK इनमें से कुछ पकड़ता है, न FastMCP; हम चाहेंगे कि आपको यह किसी host से पहले पता चल जाए।
Inline HTML से आगे
add_html_resource आम मामले को संभालता है: HTML की एक string। बाकी किसी भी चीज़ के लिए,
disk पर रखा HTML हो या generate किया गया content, resource खुद बनाएँ और सौंप दें:
from pathlib import Path
from mcp.server.apps import Apps
from mcp.server.mcpserver import MCPServer
from mcp.server.mcpserver.resources import FileResource
REPORT_HTML = Path(__file__).parent / "report.html"
apps = Apps()
@apps.tool(resource_uri="ui://report/app.html")
def refresh_report() -> str:
"""Refresh the report data."""
return "report refreshed"
apps.add_resource(FileResource(uri="ui://report/app.html", name="report", path=REPORT_HTML))
mcp = MCPServer("report", extensions=[apps])
जब resource कोई MIME type साफ़ तौर पर set नहीं करता, तो add_resource
text/html;profile=mcp-app MIME type भर देता है, और साफ़ तौर पर दिए गए बेमेल type को
reject कर देता है: किसी और MIME type वाला ui:// resource ऐसा resource है जिसे कोई host render नहीं करेगा।
Tip
क्या आप ऐसे pre-GA host के लिए बना रहे हैं जो अब भी deprecated flat
_meta["ui/resourceUri"] key पढ़ता है? इसे खुद merge करें:
@apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"})।
nested ui object ही spec वाला आकार है; flat key हटने वाली है।
इसे चलता देखें
examples/stories/ में apps story यही page एक चलाने लायक जोड़ी के रूप में है: UI-bound
clock tool वाला एक server, और एक client जो Apps negotiate करता है, tool का
_meta.ui.resourceUri पढ़ता है, HTML fetch करता है और tool को call करता है।
uv run python -m stories.apps.client