diff --git a/.github/workflows/shared.yml b/.github/workflows/shared.yml index b7b39bbaf5..a8b367c6ad 100644 --- a/.github/workflows/shared.yml +++ b/.github/workflows/shared.yml @@ -36,6 +36,9 @@ jobs: - name: Workflow script tests run: node --test '.github/scripts/*.test.js' + - name: MCP Apps UI script tests + run: node --test tests/docs_src/test_apps.test.js + - name: Surface types match vendored schema run: | uv sync --group codegen --frozen diff --git a/docs/advanced/apps.md b/docs/advanced/apps.md index 3b6232cad8..b15323c920 100644 --- a/docs/advanced/apps.md +++ b/docs/advanced/apps.md @@ -20,7 +20,7 @@ then come back. ## A clock with a face -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -51,7 +51,7 @@ The model reads `content`; the iframe is for humans. A UI-capable host still fee the text result to the model, and a text-only client gets *only* that. So the canonical pattern is one tool, two answers. Look at `get_time` again: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/docs_src/apps/tutorial001.py b/docs_src/apps/tutorial001.py index 6cbefdff7a..f62e3016a3 100644 --- a/docs_src/apps/tutorial001.py +++ b/docs_src/apps/tutorial001.py @@ -7,10 +7,29 @@ Clock

...

""" diff --git a/examples/stories/apps/server.py b/examples/stories/apps/server.py index 74d412e02c..1b4ad79ebb 100644 --- a/examples/stories/apps/server.py +++ b/examples/stories/apps/server.py @@ -17,10 +17,29 @@ Current time

…

""" diff --git a/i18n/de/pages/advanced/apps.md b/i18n/de/pages/advanced/apps.md index 266de0496d..1ad8fc751d 100644 --- a/i18n/de/pages/advanced/apps.md +++ b/i18n/de/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ Das SDK liefert das als eingebaute Extension `Apps` (`io.modelcontextprotocol/ui ## Eine Uhr mit Gesicht {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ Nicht jeder Client rendert Apps. Die Spezifikation sagt unverblümt, was das fü Das Modell liest `content`; der iframe ist für Menschen. Ein UI-fähiger Host füttert das Modell trotzdem mit dem Textergebnis, und ein reiner Text-Client bekommt *nur* das. Das kanonische Muster ist also: ein Tool, zwei Antworten. Sieh dir `get_time` noch einmal an: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/es/pages/advanced/apps.md b/i18n/es/pages/advanced/apps.md index 163ceb906e..46ae17aa48 100644 --- a/i18n/es/pages/advanced/apps.md +++ b/i18n/es/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -25,7 +25,7 @@ y luego vuelve. ## Un reloj con cara visible {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -56,7 +56,7 @@ El modelo lee `content`; el iframe es para humanos. Un host capaz de mostrar UI el resultado en texto al modelo, y un cliente solo de texto recibe *solo* eso. Así que el patrón canónico es una herramienta, dos respuestas. Mira `get_time` de nuevo: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/fr/pages/advanced/apps.md b/i18n/fr/pages/advanced/apps.md index 414eee7e62..a315dc9967 100644 --- a/i18n/fr/pages/advanced/apps.md +++ b/i18n/fr/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ Le SDK fournit cela sous la forme de l’extension intégrée `Apps` (`io.modelc ## Une horloge avec un cadran {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ Tous les clients n’affichent pas les apps. La spécification dit sans détour Le modèle lit `content` ; l’iframe est pour les humains. Un hôte capable d’afficher une interface transmet quand même le résultat textuel au modèle, et un client purement textuel ne reçoit *que* cela. Le schéma canonique est donc : un outil, deux réponses. Regardez à nouveau `get_time` : -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/hi/pages/advanced/apps.md b/i18n/hi/pages/advanced/apps.md index efaea6c3a6..cf57545323 100644 --- a/i18n/hi/pages/advanced/apps.md +++ b/i18n/hi/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK इसे built-in `Apps` extension (`io.modelcontextprotocol/ui`) के ## चेहरे वाली घड़ी {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -49,7 +49,7 @@ model `content` पढ़ता है; iframe इंसानों के ल model को देता है, और text-only client को **सिर्फ़** वही मिलता है। इसलिए मानक pattern है: एक tool, दो जवाब। `get_time` को फिर से देखें: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/ja/pages/advanced/apps.md b/i18n/ja/pages/advanced/apps.md index d1b4bf587e..d211cb10fc 100644 --- a/i18n/ja/pages/advanced/apps.md +++ b/i18n/ja/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK はこれを組み込みの `Apps` 拡張(`io.modelcontextprotocol/ui`) ## 見た目のある時計 {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ HTML 自体はホストの `postMessage` を待ち受けて結果を表示しま モデルが読むのは `content` で、iframe は人間のためのものです。UI に対応したホストでもテキストの結果はモデルに渡されますし、テキスト専用のクライアントはそれ「だけ」を受け取ります。ですから定番のパターンは「1 つのツール、2 つの答え」です。もう一度 `get_time` を見てください。 -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/ko/pages/advanced/apps.md b/i18n/ko/pages/advanced/apps.md index 2b10642ac7..1e68a1a7d2 100644 --- a/i18n/ko/pages/advanced/apps.md +++ b/i18n/ko/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK는 이를 내장 `Apps` 확장(`io.modelcontextprotocol/ui`)으로 제공합 ## 얼굴을 가진 시계 {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ HTML 자체는 호스트의 `postMessage`를 수신하고 결과를 표시합니 모델은 `content`를 읽고, iframe은 사람을 위한 것입니다. UI를 지원하는 호스트도 여전히 텍스트 결과를 모델에 전달하며, 텍스트 전용 클라이언트는 **오직** 그 텍스트만 받습니다. 따라서 표준 패턴은 도구 하나에 답 둘입니다. `get_time`을 다시 살펴보세요. -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/pt/pages/advanced/apps.md b/i18n/pt/pages/advanced/apps.md index 49a412660f..273da30ea5 100644 --- a/i18n/pt/pages/advanced/apps.md +++ b/i18n/pt/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -25,7 +25,7 @@ e depois volte aqui. ## Um relógio com uma cara {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -56,7 +56,7 @@ O modelo lê `content`; o iframe é para humanos. Um host com suporte a UI ainda o resultado em texto para o modelo, e um cliente só de texto recebe *apenas* isso. Então o padrão canônico é uma ferramenta, duas respostas. Olhe `get_time` de novo: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/ru/pages/advanced/apps.md b/i18n/ru/pages/advanced/apps.md index 772437d023..1d91c20838 100644 --- a/i18n/ru/pages/advanced/apps.md +++ b/i18n/ru/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ translation: ## Часы с циферблатом {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ translation: Модель читает `content`; iframe — для людей. Хост с поддержкой UI всё равно передаёт текстовый результат модели, а чисто текстовый клиент получает *только* его. Поэтому канонический паттерн — один инструмент, два ответа. Взгляните на `get_time` ещё раз: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/tr/pages/advanced/apps.md b/i18n/tr/pages/advanced/apps.md index dbe3380688..18207f063b 100644 --- a/i18n/tr/pages/advanced/apps.md +++ b/i18n/tr/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK bunu yerleşik `Apps` uzantısı (`io.modelcontextprotocol/ui`) olarak sunar ## Yüzü olan bir saat {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ Her istemci uygulamaları çizmez. Şartname bunun sizin için ne anlama geldiğ 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: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/uk/pages/advanced/apps.md b/i18n/uk/pages/advanced/apps.md index 76f5f941fe..12c6b34fae 100644 --- a/i18n/uk/pages/advanced/apps.md +++ b/i18n/uk/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK постачає це як вбудоване розширення `Apps` (` ## Годинник із циферблатом {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ SDK постачає це як вбудоване розширення `Apps` (` Модель читає `content`; iframe — для людей. Хост із підтримкою UI все одно передає текстовий результат моделі, а суто текстовий клієнт отримує *лише* його. Тож канонічний шаблон — один інструмент, дві відповіді. Погляньте на `get_time` ще раз: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/zh-hant/pages/advanced/apps.md b/i18n/zh-hant/pages/advanced/apps.md index 73884b17ec..22cf4f6bf6 100644 --- a/i18n/zh-hant/pages/advanced/apps.md +++ b/i18n/zh-hant/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK 以內建的 `Apps` 擴充功能(`io.modelcontextprotocol/ui`)提供這 ## 有錶面的時鐘 {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ HTML 本身會監聽主機的 `postMessage` 並顯示結果。真正的 app 請 模型讀的是 `content`;iframe 是給人看的。支援 UI 的主機照樣會把文字結果餵給模型,而純文字用戶端**只**會拿到那個。所以標準做法是一個工具,兩種答案。再看一次 `get_time`: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/i18n/zh/pages/advanced/apps.md b/i18n/zh/pages/advanced/apps.md index 1cc37465a5..3d0d76d26e 100644 --- a/i18n/zh/pages/advanced/apps.md +++ b/i18n/zh/pages/advanced/apps.md @@ -1,6 +1,6 @@ --- translation: - sections: [0355618e5f4d5fe4, 0fefa31fb7c7b585, 5c53e7487c9c70cc, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] + sections: [0355618e5f4d5fe4, b4ac54f338092ec6, 1419418a05511012, 8ac39614c094f2d0, dab6ff945501ab2a, bd5565c3b2d4f959, 96819ce3d63a0487] tool: 1 --- # MCP Apps {#mcp-apps} @@ -18,7 +18,7 @@ SDK 把它作为内置的 `Apps` 扩展(`io.modelcontextprotocol/ui`)提供 ## 一个带界面的时钟 {#a-clock-with-a-face} -```python title="server.py" hl_lines="17 20 28 30" +```python title="server.py" hl_lines="36 39 47 49" --8<-- "docs_src/apps/tutorial001.py" ``` @@ -39,7 +39,7 @@ HTML 本身监听宿主的 `postMessage` 并显示结果。真正的应用请在 模型读的是 `content`;iframe 是给人看的。支持 UI 的宿主照样会把文本结果交给模型,而纯文本客户端**只**拿到那部分。所以标准模式是一个工具、两种回答。再看一遍 `get_time`: -```python title="server.py" hl_lines="21-25" +```python title="server.py" hl_lines="40-44" --8<-- "docs_src/apps/tutorial001.py" ``` diff --git a/tests/docs_src/test_apps.test.js b/tests/docs_src/test_apps.test.js new file mode 100644 index 0000000000..62ce079690 --- /dev/null +++ b/tests/docs_src/test_apps.test.js @@ -0,0 +1,95 @@ +const assert = require("node:assert/strict"); +const { execFileSync } = require("node:child_process"); +const path = require("node:path"); +const test = require("node:test"); +const vm = require("node:vm"); + +const root = path.resolve(__dirname, "../.."); +const python = ` +import json + +import anyio + +from docs_src.apps import tutorial001 +from mcp import Client +from mcp.client import advertise +from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID +from mcp.types import TextResourceContents +from stories.apps.server import RESOURCE_URI, build_server + + +async def main() -> None: + cases = [] + for name, server, uri in ( + ("tutorial", tutorial001.mcp, "ui://clock/app.html"), + ("story", build_server(), RESOURCE_URI), + ): + async with Client(server, extensions=[advertise(EXTENSION_ID, {"mimeTypes": [APP_MIME_TYPE]})]) as client: + resource = await client.read_resource(uri) + result = await client.call_tool("get_time", {}) + content = resource.contents[0] + assert isinstance(content, TextResourceContents) + assert content.mime_type == APP_MIME_TYPE + cases.append({"name": name, "html": content.text, "result": result.model_dump(mode="json", by_alias=True)}) + print(json.dumps(cases)) + + +anyio.run(main) +`; + +const cases = JSON.parse( + execFileSync("uv", ["run", "--frozen", "--no-sync", "python", "-c", python], { + cwd: root, + encoding: "utf8", + env: { + ...process.env, + PYTHONPATH: [root, path.join(root, "examples"), process.env.PYTHONPATH] + .filter(Boolean) + .join(path.delimiter), + }, + }), +); + +for (const { name, html, result } of cases) { + test(`${name} clock shows the tool-result notification`, () => { + const script = html.match(/