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:
- İşi yapan ve veri döndüren bir araç, tıpkı diğer araçlar gibi.
- 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
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.resourceUridamgası.@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-appolarak 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ıkcapabilities.extensionsaltındaio.modelcontextprotocol/uiduyurur.
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
contentdizisi 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:
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:
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 (callServerToolaracı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 birresource_uriveya kaynak URI'si, dekoratör/kayıt anında birValueError'dır.- Eşleşen kayıtlı bir kaynağı olmayan bir URI'ye bağlanmış araç,
MCPServer(extensions=[apps])uzantıyı tükettiğinde birValueError'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()üzerindemeta={"ui": ...}birValueError'dır._meta["ui"]dekoratöre aittir; bunuresource_uri=vevisibility=ile söyleyin. Diğermeta=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:
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