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