विषय पर बढ़ें

MCP Apps

मशीनी अनुवाद

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

MCP App ऐसा tool है जिसका एक चेहरा है: अपने data के साथ-साथ tool एक HTML document की ओर इशारा करता है, जिसे host interactive surface के रूप में render करता है।

दो हिस्से, हमेशा दो हिस्से:

  1. एक tool, जो काम करता है और data लौटाता है, किसी भी दूसरे tool की तरह।
  2. एक 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 पर एक नज़र डाल लें। एक मिनट लगेगा, फिर वापस आएँ।

चेहरे वाली घड़ी

server.py
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/ui advertise करता है।

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 का एक सार्थक content array लौटाना अनिवार्य (MUST) है।

model content पढ़ता है; iframe इंसानों के लिए है। UI-capable host भी text result model को देता है, और text-only client को सिर्फ़ वही मिलता है। इसलिए मानक pattern है: एक tool, दो जवाब। get_time को फिर से देखें:

server.py
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 होना चाहेगा:

server.py
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 URI ui://... नहीं है, वह 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 खुद बनाएँ और सौंप दें:

server.py
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