Ana içeriğe geç

MCP Apps

Makine çevirisi

Bu sayfa İngilizce dokümantasyondan otomatik olarak çevrildi; esas alınması gereken sürüm İngilizce sayfadır. Yanlış görünen bir şey varsa, nasıl bildireceğinizi Çeviriler sayfası açıklar.

Bir MCP App, yüzü olan bir araçtır: araç, verisinin yanında host'un etkileşimli bir yüzey olarak çizdiği bir HTML belgesine de işaret eder.

İki parça, her zaman iki parça:

  1. İşi yapan ve veri döndüren bir araç, tıpkı diğer araçlar gibi.
  2. Host'un onun için gösterdiği HTML'i içeren bir ui:// kaynağı.

Araç, kaynağa işaret eden bir _meta.ui.resourceUri referansı taşır. Host onu resources/read ile getirir, korumalı (sandboxed) bir iframe içinde çizer ve aracın sonucunu postMessage aracılığıyla bu iframe'e iter. Sunucu hiçbir ui/* mesajı göndermez ve almaz: bu trafik host ile iframe arasındadır. Siz bir araç ve bir HTML belgesi sunarsınız; gösteriyi host sahneler.

SDK bunu yerleşik Apps uzantısı (io.modelcontextprotocol/ui) olarak sunar. Uzantılar size yeniyse önce o sayfaya göz atın. Bir dakika, sonra geri dönün.

Yüzü olan bir saat

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')]

Dört hamle:

  • Apps(): tek bir örnek, UI'ya bağlı araçlarınızı ve onların kaynaklarını tutar.
  • @apps.tool(resource_uri="ui://clock/app.html"): sıradan bir araç, artı _meta.ui.resourceUri damgası. @mcp.tool()'un kabul ettiği her şey (name, title, description, ...) aynen geçer.
  • apps.add_html_resource("ui://clock/app.html", CLOCK_HTML): eşleşen kaynak, text/html;profile=mcp-app olarak sunulur. Bir host'a "bu bir uygulama, çiz" diyen şey tam olarak bu MIME türüdür.
  • MCPServer("clock", extensions=[apps]): katılımı açın. Sunucu artık capabilities.extensions altında io.modelcontextprotocol/ui duyurur.

HTML'in kendisi host'un postMessage'ını dinler ve sonucu gösterir. Gerçek uygulamalar için HTML'inizin içinde resmi @modelcontextprotocol/ext-apps tarayıcı SDK'sını kullanın. Ham mesaj olayları yerine size ontoolresult, callServerTool, getHostContext ve onhostcontextchanged verir.

Zarifçe geri çekilme

Her istemci uygulamaları çizmez. Şartname bunun sizin için ne anlama geldiğini açıkça söyler:

UI mevcut olsa bile araçlar anlamlı bir content dizisi döndürmek ZORUNDADIR.

Model content'i okur; iframe insanlar içindir. UI destekli bir host yine de metin sonucunu modele iletir, yalnızca metin destekleyen bir istemci ise sadece onu alır. Yani kanonik desen tek araç, iki yanıttır. get_time'a bir daha bakın:

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) yalnızca istemci io.modelcontextprotocol/ui uzantısını beyan ettiğinde ve mimeTypes ayarlarında text/html;profile=mcp-app'i listelediğinde True olur. Alan zorunludur, bu yüzden onu atlayan bir istemci sayılmaz. Aynı dosyadaki main() tam olarak bunu beyan eder: anlaşmanın istemci tarafı, ve zengin yanıt geri gelir.

Warning

Tek içerik olarak asla "[Rendered UI]" gibi bir yer tutucu döndürmeyin. Yedek metin işe yaramazsa araç, yalnızca metin destekleyen her istemci için ve modelin kendisi için işe yaramaz. O cümleyi yazın.

iframe'i kilitleme

Güvenlik metaverisini kaynak tarafı taşır: iframe'in neleri yükleyebileceği, hangi tarayıcı izinlerini istediği, nasıl çerçevelenmek istediği:

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 ve permissions sunucu davranışı değil, host'a yapılan isteklerdir. Host, iframe'in Content-Security-Policy ve Permissions-Policy değerlerini bunlardan oluşturur ve reddedebilir. İznin verildiğini varsaymak yerine JS kodunuzda özellik algılaması yapın.

ResourceCsp, alan alan (Python adı, iletilen verideki anahtar, host'un onunla ne yaptığı):

Python İletilen veri (_meta.ui.csp) Denetlediği
connect_domains connectDomains connect-src: fetch/XHR nereye gidebilir
resource_domains resourceDomains img-src, style-src, ...: statik varlıklar
frame_domains frameDomains frame-src: iç içe iframe'ler
base_uri_domains baseUriDomains base-uri: <base> nereye işaret edebilir

ResourcePermissions: her alan iframe için bir tarayıcı izni ister.

Python İletilen veri (_meta.ui.permissions)
camera camera
microphone microphone
geolocation geolocation
clipboard_write clipboardWrite

Note

CSP ve izinler kaynak üzerinde yaşar, asla araç üzerinde değil. Şartnamenin araç metaverisinde bunlar için bir yer yoktur ve host'lar orada onları yok sayar. SDK bu hatayı ifade edilemez kılar: @apps.tool()'un csp parametresi yoktur.

Görünürlük

Bir araçtaki visibility=["app"], "bu, model için değil iframe için var" der:

  • "model": model onu çağırabilir.
  • "app": iframe onu çağırabilir (callServerTool aracılığıyla).
  • Belirtilmezse: ikisi de, varsayılan budur.

Filtreleme host'un işidir. Sunucu yalnızca uygulamaya özel araçları tools/list içinde diğerleri gibi listeler; host onları modelden gizler. Sunucu tarafında filtrelemeyin.

SDK'nın uyguladığı kurallar

Bunların hepsi üretimde değil, başlangıçta hata verir:

  • ui://... olmayan bir resource_uri veya kaynak URI'si, dekoratör/kayıt anında bir ValueError'dır.
  • Eşleşen kayıtlı bir kaynağı olmayan bir URI'ye bağlanmış araç, MCPServer(extensions=[apps]) uzantıyı tükettiğinde bir ValueError'dır. resources/read'de 404 dönen bir HTML duyuran araç bir yanlış yapılandırmadır, bu yüzden oluşturmayı reddeder.
  • @apps.tool() üzerinde meta={"ui": ...} bir ValueError'dır. _meta["ui"] dekoratöre aittir; bunu resource_uri= ve visibility= ile söyleyin. Diğer meta= anahtarları yanına sorunsuzca birleşir.

Bugün ne TypeScript ext-apps SDK'sı ne de FastMCP bunların herhangi birini yakalar; bir host'tan önce sizin öğrenmenizi tercih ederiz.

Satır içi HTML'in ötesi

add_html_resource yaygın durumu karşılar: bir HTML dizesi. Bunun dışındaki her şey için (diskteki HTML veya üretilen içerik) kaynağı kendiniz oluşturup teslim edin:

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])

add_resource, kaynak açıkça bir MIME türü belirtmediğinde text/html;profile=mcp-app MIME türünü doldurur ve açık bir uyuşmazlığı reddeder: başka herhangi bir MIME türü altındaki ui:// kaynağını hiçbir host çizmez.

Tip

Hâlâ kullanım dışı bırakılmış düz _meta["ui/resourceUri"] anahtarını okuyan GA öncesi bir host'u mu hedefliyorsunuz? Kendiniz birleştirin: @apps.tool(resource_uri="ui://x", meta={"ui/resourceUri": "ui://x"}). İç içe ui nesnesi şartnamedeki biçimdir; düz anahtar kaldırılma yolunda.

Çalışırken görün

examples/stories/ içindeki apps hikâyesi, bu sayfanın çalıştırılabilir bir çift hâlidir: UI'ya bağlı bir saat aracı olan bir sunucu ve Apps anlaşmasını yapan, aracın _meta.ui.resourceUri değerini okuyan, HTML'i getiren ve aracı çağıran bir istemci.

uv run python -m stories.apps.client