Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs_src/apps/tutorial001.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<h1 id="now">...</h1>
<script>
window.addEventListener("message", (event) => {
const text = event.data?.result?.content?.[0]?.text;
const text = event.data?.params?.content?.[0]?.text;
Comment thread
Kludex marked this conversation as resolved.
Resolved
if (text) document.getElementById("now").textContent = text;
});
</script>
Expand Down
2 changes: 1 addition & 1 deletion examples/stories/apps/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<h1 id="now">…</h1>
<script>
window.addEventListener("message", (event) => {
const text = event.data?.result?.content?.[0]?.text;
const text = event.data?.params?.content?.[0]?.text;
Comment thread
Kludex marked this conversation as resolved.
Resolved
if (text) document.getElementById("now").textContent = text;
});
</script>
Expand Down
78 changes: 78 additions & 0 deletions tests/docs_src/test_apps.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
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),
},
}),
);
Comment thread
Kludex marked this conversation as resolved.
Resolved

Comment thread
Kludex marked this conversation as resolved.
Resolved
for (const { name, html, result } of cases) {
test(`${name} clock shows the tool-result notification`, () => {
const script = html.match(/<script>([\s\S]*?)<\/script>/)?.[1];
assert.ok(script);
const clock = { textContent: "..." };
let onMessage;
vm.runInNewContext(script, {
window: {
addEventListener(type, handler) {
assert.equal(type, "message");
onMessage = handler;
},
},
document: {
getElementById(id) {
assert.equal(id, "now");
return clock;
},
},
});

assert.equal(typeof onMessage, "function");
onMessage({ data: { jsonrpc: "2.0", method: "ui/notifications/tool-result", params: result } });
assert.equal(clock.textContent, result.content[0].text);
});
}
Loading