Skip to content

Commit e39dbe2

Browse files
committed
Test MCP Apps clock tool-result notifications
1 parent bf951da commit e39dbe2

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

‎.github/workflows/shared.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ jobs:
3636
- name: Workflow script tests
3737
run: node --test '.github/scripts/*.test.js'
3838

39+
- name: MCP Apps UI script tests
40+
run: node --test tests/docs_src/test_apps.test.js
41+
3942
- name: Surface types match vendored schema
4043
run: |
4144
uv sync --group codegen --frozen

‎tests/docs_src/test_apps.test.js‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const assert = require("node:assert/strict");
2+
const { execFileSync } = require("node:child_process");
3+
const path = require("node:path");
4+
const test = require("node:test");
5+
const vm = require("node:vm");
6+
7+
const root = path.resolve(__dirname, "../..");
8+
const python = `
9+
import json
10+
11+
import anyio
12+
13+
from docs_src.apps import tutorial001
14+
from mcp import Client
15+
from mcp.client import advertise
16+
from mcp.server.apps import APP_MIME_TYPE, EXTENSION_ID
17+
from mcp.types import TextResourceContents
18+
from stories.apps.server import RESOURCE_URI, build_server
19+
20+
21+
async def main() -> None:
22+
cases = []
23+
for name, server, uri in (
24+
("tutorial", tutorial001.mcp, "ui://clock/app.html"),
25+
("story", build_server(), RESOURCE_URI),
26+
):
27+
async with Client(server, extensions=[advertise(EXTENSION_ID, {"mimeTypes": [APP_MIME_TYPE]})]) as client:
28+
resource = await client.read_resource(uri)
29+
result = await client.call_tool("get_time", {})
30+
content = resource.contents[0]
31+
assert isinstance(content, TextResourceContents)
32+
assert content.mime_type == APP_MIME_TYPE
33+
cases.append({"name": name, "html": content.text, "result": result.model_dump(mode="json", by_alias=True)})
34+
print(json.dumps(cases))
35+
36+
37+
anyio.run(main)
38+
`;
39+
40+
const cases = JSON.parse(
41+
execFileSync("uv", ["run", "--frozen", "--no-sync", "python", "-c", python], {
42+
cwd: root,
43+
encoding: "utf8",
44+
env: {
45+
...process.env,
46+
PYTHONPATH: [root, path.join(root, "examples"), process.env.PYTHONPATH]
47+
.filter(Boolean)
48+
.join(path.delimiter),
49+
},
50+
}),
51+
);
52+
53+
for (const { name, html, result } of cases) {
54+
test(`${name} clock shows the tool-result notification`, () => {
55+
const script = html.match(/<script>([\s\S]*?)<\/script>/)?.[1];
56+
assert.ok(script);
57+
const clock = { textContent: "..." };
58+
let onMessage;
59+
vm.runInNewContext(script, {
60+
window: {
61+
addEventListener(type, handler) {
62+
assert.equal(type, "message");
63+
onMessage = handler;
64+
},
65+
},
66+
document: {
67+
getElementById(id) {
68+
assert.equal(id, "now");
69+
return clock;
70+
},
71+
},
72+
});
73+
74+
assert.equal(typeof onMessage, "function");
75+
onMessage({ data: { jsonrpc: "2.0", method: "ui/notifications/tool-result", params: result } });
76+
assert.equal(clock.textContent, result.content[0].text);
77+
});
78+
}

0 commit comments

Comments
 (0)