@emseepea/create-react-ui-server 0.0.25 → 0.0.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/initializer-dist/template/README.md +4 -0
- package/initializer-dist/template/package.json +5 -5
- package/initializer-dist/template/src/capabilities/resource.pea-planting-plan-app.ts +11 -47
- package/initializer-dist/template/src/mcp-app.tsx +134 -0
- package/initializer-dist/template/test/server.test.mjs +71 -6
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -72,6 +72,10 @@ The MCP endpoint also publishes an opt-in MCP Apps result card for the preview
|
|
|
72
72
|
tool. See the [UI example guide](https://emseepea.github.io/emseepea/examples/#add-a-web-form-or-mcp-app)
|
|
73
73
|
for the resource contract and ChatGPT compatibility fields.
|
|
74
74
|
|
|
75
|
+
The result card maps the tool's domain output into the shared bounded result
|
|
76
|
+
model, renders it with `ResultCard`, and uses `useMcpApp` for the MCP Apps host
|
|
77
|
+
lifecycle. The example keeps only its domain parser, mapping, and action text.
|
|
78
|
+
|
|
75
79
|
The page previews a sample pea planting plan only. It does not send or store a report.
|
|
76
80
|
|
|
77
81
|
## Build a production container
|
|
@@ -54,6 +54,10 @@ The MCP endpoint also publishes an opt-in MCP Apps result card for the preview
|
|
|
54
54
|
tool. See the [UI example guide](https://emseepea.github.io/emseepea/examples/#add-a-web-form-or-mcp-app)
|
|
55
55
|
for the resource contract and ChatGPT compatibility fields.
|
|
56
56
|
|
|
57
|
+
The result card maps the tool's domain output into the shared bounded result
|
|
58
|
+
model, renders it with `ResultCard`, and uses `useMcpApp` for the MCP Apps host
|
|
59
|
+
lifecycle. The example keeps only its domain parser, mapping, and action text.
|
|
60
|
+
|
|
57
61
|
The page previews a sample pea planting plan only. It does not send or store a report.
|
|
58
62
|
|
|
59
63
|
## Build a production container
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"build": "tsc -p tsconfig.json && esbuild src/client.tsx --bundle --minify --format=esm --platform=browser --outfile=dist/client.js",
|
|
8
|
+
"build": "tsc -p tsconfig.json && esbuild src/client.tsx --bundle --minify --format=esm --platform=browser --outfile=dist/client.js && esbuild src/mcp-app.tsx --bundle --minify --format=esm --platform=browser --outfile=dist/mcp-app-client.js",
|
|
9
9
|
"start": "node dist/server.js",
|
|
10
10
|
"test": "npm run build && npm run test:built",
|
|
11
11
|
"test:built": "node --test test/*.test.mjs",
|
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
"container:build": "docker build --tag emseepea-server:local ."
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
-
"@emseepea/feedback": "0.2.
|
|
18
|
+
"@emseepea/feedback": "0.2.10",
|
|
19
19
|
"playwright": "1.62.1",
|
|
20
20
|
"axe-core": "4.13.0",
|
|
21
|
-
"@emseepea/testing": "0.9.
|
|
21
|
+
"@emseepea/testing": "0.9.14",
|
|
22
22
|
"@types/node": "24.13.3",
|
|
23
23
|
"@types/react": "19.2.18",
|
|
24
24
|
"@types/react-dom": "19.2.5",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"private": true,
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@emseepea/react": "0.0
|
|
35
|
-
"@emseepea/server": "0.
|
|
34
|
+
"@emseepea/react": "0.1.0",
|
|
35
|
+
"@emseepea/server": "0.11.0",
|
|
36
36
|
"@emseepea/tailwind": "0.0.2",
|
|
37
37
|
"react": "19.2.8",
|
|
38
38
|
"react-dom": "19.2.8"
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { plantingPlanAppResourceUri } from "../ui-shared.js";
|
|
2
2
|
import { defineResource, type AccessPolicy, type CapabilityModuleFactory } from "@emseepea/server";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { createElement } from "react";
|
|
5
|
+
import { renderToString } from "react-dom/server";
|
|
6
|
+
import { ResultApp } from "../mcp-app.js";
|
|
3
7
|
|
|
4
8
|
const csp = { connectDomains: [], resourceDomains: [] };
|
|
5
9
|
const legacyCsp = { connect_domains: [], resource_domains: [] };
|
|
@@ -25,50 +29,10 @@ export default ((access) => defineResource({
|
|
|
25
29
|
}),
|
|
26
30
|
})) satisfies CapabilityModuleFactory<AccessPolicy>;
|
|
27
31
|
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const initializeId = 1;
|
|
36
|
-
const connectionTimeout = setTimeout(() => {
|
|
37
|
-
status.textContent = "The preview could not connect to its host.";
|
|
38
|
-
}, 10000);
|
|
39
|
-
window.addEventListener("message", (event) => {
|
|
40
|
-
if (event.source !== window.parent) return;
|
|
41
|
-
if (event.data?.id === initializeId) {
|
|
42
|
-
clearTimeout(connectionTimeout);
|
|
43
|
-
if (event.data.error) {
|
|
44
|
-
status.textContent = "The preview could not connect to its host.";
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
window.parent.postMessage({ jsonrpc: "2.0", method: "ui/notifications/initialized" }, "*");
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
if (event.data?.method === "ui/notifications/tool-cancelled") {
|
|
51
|
-
status.textContent = "The preview was cancelled.";
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (event.data?.method !== "ui/notifications/tool-result") return;
|
|
55
|
-
const result = event.data.params?.structuredContent;
|
|
56
|
-
if (!Number.isInteger(result?.matchingCount) || !Array.isArray(result?.varieties)) {
|
|
57
|
-
status.textContent = "The preview result could not be displayed.";
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
status.textContent = result.matchingCount + (result.matchingCount === 1 ? " sample variety matches." : " sample varieties match.");
|
|
61
|
-
list.replaceChildren(...result.varieties.flatMap((variety) =>
|
|
62
|
-
typeof variety?.name === "string" ? [Object.assign(document.createElement("li"), { textContent: variety.name })] : []));
|
|
63
|
-
}, { passive: true });
|
|
64
|
-
window.parent.postMessage({
|
|
65
|
-
jsonrpc: "2.0",
|
|
66
|
-
id: initializeId,
|
|
67
|
-
method: "ui/initialize",
|
|
68
|
-
params: {
|
|
69
|
-
protocolVersion: "2026-01-26",
|
|
70
|
-
appInfo: { name: "Pea planting plan result", version: "1.0.0" },
|
|
71
|
-
appCapabilities: { availableDisplayModes: ["inline"] },
|
|
72
|
-
},
|
|
73
|
-
}, "*");
|
|
74
|
-
</script></body></html>`;
|
|
32
|
+
const appScript = readFileSync(new URL("../mcp-app-client.js", import.meta.url), "utf8").replaceAll("</script", "<\\/script");
|
|
33
|
+
const appHtml = `<!doctype html><html lang="en"><head><meta charset="utf-8">` +
|
|
34
|
+
`<meta name="viewport" content="width=device-width, initial-scale=1">` +
|
|
35
|
+
`<title>Pea planting plan result</title><style>` +
|
|
36
|
+
`button{min-block-size:24px} :focus-visible{outline:2px solid currentColor;outline-offset:2px}` +
|
|
37
|
+
`</style></head><body><main id="app">` +
|
|
38
|
+
`${renderToString(createElement(ResultApp))}</main><script type="module">${appScript}</script></body></html>`;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { ResultCard, useMcpApp } from "@emseepea/react";
|
|
2
|
+
import { defineResultView, type ResultView as ResultViewModel } from "@emseepea/server/ui";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { hydrateRoot } from "react-dom/client";
|
|
5
|
+
|
|
6
|
+
interface PlantingPlanResult {
|
|
7
|
+
readonly title: string;
|
|
8
|
+
readonly matchingCount: number;
|
|
9
|
+
readonly varieties: readonly {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly growthHabit: "bush" | "climbing";
|
|
12
|
+
readonly peaType: "shelling" | "snap";
|
|
13
|
+
}[];
|
|
14
|
+
readonly notice: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ResultApp() {
|
|
18
|
+
const app = useMcpApp({ name: "Pea planting plan result", version: "1.0.0", parseResult: parsePlantingPlan });
|
|
19
|
+
const [action, setAction] = useState<"idle" | "sending" | "sent" | "error">("idle");
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (app.hostContext.theme) document.documentElement.dataset.emseepeaTheme = app.hostContext.theme;
|
|
23
|
+
}, [app.hostContext.theme]);
|
|
24
|
+
|
|
25
|
+
const view = resultView(app, action);
|
|
26
|
+
const act = async () => {
|
|
27
|
+
if (action !== "idle") return;
|
|
28
|
+
setAction("sending");
|
|
29
|
+
try {
|
|
30
|
+
await app.sendMessage("Show me growing tips for these pea varieties.");
|
|
31
|
+
setAction("sent");
|
|
32
|
+
} catch {
|
|
33
|
+
setAction("error");
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<>
|
|
39
|
+
<h1>Pea planting plan result</h1>
|
|
40
|
+
<ResultCard view={view} headingLevel={2} idPrefix="pea-result" onAction={() => void act()} />
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resultView(
|
|
46
|
+
app: ReturnType<typeof useMcpApp<PlantingPlanResult>>,
|
|
47
|
+
action: "idle" | "sending" | "sent" | "error",
|
|
48
|
+
): ResultViewModel {
|
|
49
|
+
const base = {
|
|
50
|
+
id: "pea-planting-plan-result",
|
|
51
|
+
heading: app.result?.title ?? "Planting plan preview",
|
|
52
|
+
disclaimer: app.result?.notice ?? "No report will be sent or stored.",
|
|
53
|
+
};
|
|
54
|
+
if (app.status === "connecting" || app.status === "ready") {
|
|
55
|
+
return defineResultView({
|
|
56
|
+
...base,
|
|
57
|
+
state: { kind: "loading", status: "Waiting for the preview result.", focusTarget: "none" },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (app.status === "cancelled") {
|
|
61
|
+
return defineResultView({
|
|
62
|
+
...base,
|
|
63
|
+
summary: "The preview was cancelled.",
|
|
64
|
+
state: { kind: "empty", status: "The preview was cancelled.", focusTarget: "none" },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (app.status === "error" || !app.result) {
|
|
68
|
+
const error = app.error ?? "The preview result could not be displayed.";
|
|
69
|
+
return defineResultView({ ...base, summary: error, state: { kind: "error", status: error, focusTarget: "status" } });
|
|
70
|
+
}
|
|
71
|
+
const count = app.result.matchingCount;
|
|
72
|
+
const headline = `${count} sample pea ${count === 1 ? "variety matches" : "varieties match"}.`;
|
|
73
|
+
const actions = [{
|
|
74
|
+
id: "growing-tips",
|
|
75
|
+
label: "Ask for growing tips",
|
|
76
|
+
accessibleName: "Ask for growing tips for these varieties",
|
|
77
|
+
disabled: action !== "idle",
|
|
78
|
+
}];
|
|
79
|
+
const actionState = action === "sending"
|
|
80
|
+
? { kind: "sending" as const, status: "Asking for growing tips.", focusTarget: "status" as const }
|
|
81
|
+
: action === "sent"
|
|
82
|
+
? { kind: "sent" as const, status: "Asked for growing tips in the chat.", focusTarget: "status" as const }
|
|
83
|
+
: action === "error"
|
|
84
|
+
? { kind: "error" as const, status: "Growing tips could not be requested. Ask in the chat instead.", focusTarget: "status" as const }
|
|
85
|
+
: {
|
|
86
|
+
kind: app.resultRevision > 1 ? "updated" as const : count === 0 ? "empty" as const : "ready" as const,
|
|
87
|
+
status: app.resultRevision > 1 ? `Preview updated: ${headline}` : headline,
|
|
88
|
+
focusTarget: "none" as const,
|
|
89
|
+
};
|
|
90
|
+
return defineResultView({
|
|
91
|
+
...base,
|
|
92
|
+
headline: count > 0 ? headline : undefined,
|
|
93
|
+
summary: count === 0 ? "No sample pea varieties match this preview." : undefined,
|
|
94
|
+
metrics: [{ label: "Matching varieties", value: String(count) }],
|
|
95
|
+
disclosure: app.result.varieties.length ? {
|
|
96
|
+
label: "Included varieties",
|
|
97
|
+
items: app.result.varieties.map(({ name, growthHabit, peaType }) => `${name}, ${growthHabit} ${peaType} pea`),
|
|
98
|
+
} : undefined,
|
|
99
|
+
actionsLabel: "Continue in chat",
|
|
100
|
+
actions,
|
|
101
|
+
state: actionState,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parsePlantingPlan(value: unknown): PlantingPlanResult {
|
|
106
|
+
if (!record(value) || typeof value.title !== "string" || value.title.length > 80 ||
|
|
107
|
+
!Number.isSafeInteger(value.matchingCount) || (value.matchingCount as number) < 0 ||
|
|
108
|
+
!Array.isArray(value.varieties) || value.varieties.length > 32 || typeof value.notice !== "string") {
|
|
109
|
+
throw new TypeError("Invalid planting-plan result");
|
|
110
|
+
}
|
|
111
|
+
const varieties = value.varieties.map((candidate) => {
|
|
112
|
+
if (!record(candidate) || typeof candidate.name !== "string" || candidate.name.length > 160 ||
|
|
113
|
+
(candidate.growthHabit !== "bush" && candidate.growthHabit !== "climbing") ||
|
|
114
|
+
(candidate.peaType !== "shelling" && candidate.peaType !== "snap")) {
|
|
115
|
+
throw new TypeError("Invalid planting-plan variety");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
name: candidate.name,
|
|
119
|
+
growthHabit: candidate.growthHabit as "bush" | "climbing",
|
|
120
|
+
peaType: candidate.peaType as "shelling" | "snap",
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
if (value.matchingCount !== varieties.length || value.notice.length > 1_000) {
|
|
124
|
+
throw new TypeError("Inconsistent planting-plan result");
|
|
125
|
+
}
|
|
126
|
+
return { title: value.title, matchingCount: value.matchingCount as number, varieties, notice: value.notice };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
130
|
+
return typeof value === "object" && value !== null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const root = typeof document === "undefined" ? null : document.querySelector<HTMLElement>("#app");
|
|
134
|
+
if (root) hydrateRoot(root, <ResultApp />);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import test from "node:test";
|
|
3
4
|
|
|
4
5
|
import {
|
|
@@ -9,6 +10,8 @@ import {
|
|
|
9
10
|
import { chromium } from "playwright";
|
|
10
11
|
import { createReactUiServer } from "../dist/app.js";
|
|
11
12
|
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
|
|
12
15
|
test("describes every planting-plan tool property", async (t) => {
|
|
13
16
|
const running = await startMcpServer(t, new URL("../dist/server.js", import.meta.url));
|
|
14
17
|
const client = await running.connect();
|
|
@@ -56,8 +59,8 @@ test("publishes the standards-first MCP Apps resource contract", async (t) => {
|
|
|
56
59
|
connect_domains: [],
|
|
57
60
|
resource_domains: [],
|
|
58
61
|
});
|
|
59
|
-
assert.match(content.text, /
|
|
60
|
-
assert.match(content.text, /
|
|
62
|
+
assert.match(content.text, /ui\/initialize/);
|
|
63
|
+
assert.match(content.text, /ui\/notifications\/initialized/);
|
|
61
64
|
assert.match(content.text, /ui\/notifications\/tool-result/);
|
|
62
65
|
assert.match(content.text, /ui\/notifications\/tool-cancelled/);
|
|
63
66
|
});
|
|
@@ -72,13 +75,15 @@ test("the MCP Apps card completes initialization before rendering a result", asy
|
|
|
72
75
|
const page = await browser.newPage();
|
|
73
76
|
await page.setContent("<iframe title=\"Pea planting plan result\"></iframe>");
|
|
74
77
|
await page.evaluate(() => {
|
|
78
|
+
window.receivedAppMessages = [];
|
|
75
79
|
window.addEventListener("message", (event) => {
|
|
80
|
+
window.receivedAppMessages.push(event.data);
|
|
76
81
|
if (event.data?.method === "ui/initialize") {
|
|
77
82
|
event.source.postMessage({ jsonrpc: "2.0", id: event.data.id, result: {
|
|
78
83
|
protocolVersion: "2026-01-26",
|
|
79
84
|
hostInfo: { name: "test-host", version: "1.0.0" },
|
|
80
85
|
hostCapabilities: {},
|
|
81
|
-
hostContext: {},
|
|
86
|
+
hostContext: { theme: "light", displayMode: "inline" },
|
|
82
87
|
} }, "*");
|
|
83
88
|
}
|
|
84
89
|
if (event.data?.method === "ui/notifications/initialized") {
|
|
@@ -86,17 +91,77 @@ test("the MCP Apps card completes initialization before rendering a result", asy
|
|
|
86
91
|
jsonrpc: "2.0",
|
|
87
92
|
method: "ui/notifications/tool-result",
|
|
88
93
|
params: { structuredContent: {
|
|
94
|
+
title: "Spring peas",
|
|
89
95
|
matchingCount: 1,
|
|
90
|
-
varieties: [{ name: "Highland Snap" }],
|
|
96
|
+
varieties: [{ name: "Highland Snap", growthHabit: "climbing", peaType: "snap" }],
|
|
97
|
+
notice: "No report was sent or stored.",
|
|
91
98
|
} },
|
|
92
99
|
}, "*");
|
|
93
100
|
}
|
|
101
|
+
if (event.data?.method === "ui/message") {
|
|
102
|
+
event.source.postMessage({ jsonrpc: "2.0", id: event.data.id, result: {} }, "*");
|
|
103
|
+
}
|
|
94
104
|
});
|
|
95
105
|
});
|
|
96
106
|
const frame = page.frames()[1];
|
|
97
107
|
await frame.setContent(content.text);
|
|
98
|
-
|
|
99
|
-
|
|
108
|
+
const status = frame.locator("[data-emseepea-part='status']");
|
|
109
|
+
await status.filter({ hasText: "1 sample pea variety matches." }).waitFor();
|
|
110
|
+
const statusNode = await status.elementHandle();
|
|
111
|
+
assert.equal(await frame.locator("dt").textContent(), "Matching varieties");
|
|
112
|
+
assert.equal(await frame.locator("dd").textContent(), "1");
|
|
113
|
+
assert.match(await frame.locator("details li").textContent(), /Highland Snap, climbing snap pea/);
|
|
114
|
+
assert.equal(await frame.locator("button").getAttribute("type"), "button");
|
|
115
|
+
assert.equal(await frame.locator("button").getAttribute("aria-label"), "Ask for growing tips for these varieties");
|
|
116
|
+
await frame.addScriptTag({ path: require.resolve("axe-core/axe.min.js") });
|
|
117
|
+
const accessibility = await frame.evaluate(() => window.axe.run(document));
|
|
118
|
+
assert.deepEqual(accessibility.violations.map(({ id }) => id), []);
|
|
119
|
+
await page.setViewportSize({ width: 320, height: 640 });
|
|
120
|
+
assert.equal(await frame.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth), true);
|
|
121
|
+
assert.ok((await frame.locator("button").boundingBox()).height >= 24);
|
|
122
|
+
await page.emulateMedia({ forcedColors: "active", reducedMotion: "reduce" });
|
|
123
|
+
await frame.locator("button").focus();
|
|
124
|
+
assert.notEqual(await frame.locator("button").evaluate((button) => getComputedStyle(button).outlineStyle), "none");
|
|
125
|
+
assert.equal(await frame.evaluate(() => matchMedia("(prefers-reduced-motion: reduce)").matches), true);
|
|
126
|
+
|
|
127
|
+
await frame.evaluate(() => window.postMessage({ jsonrpc: "2.0", method: "ui/notifications/tool-result", params: {
|
|
128
|
+
structuredContent: { title: "Forged", matchingCount: 0, varieties: [], notice: "Forged" },
|
|
129
|
+
} }, "*"));
|
|
130
|
+
assert.equal(await frame.locator("dd").textContent(), "1");
|
|
131
|
+
|
|
132
|
+
await page.evaluate(() => document.querySelector("iframe").contentWindow.postMessage({
|
|
133
|
+
jsonrpc: "2.0", method: "ui/notifications/tool-cancelled",
|
|
134
|
+
}, "*"));
|
|
135
|
+
await status.filter({ hasText: "The preview was cancelled." }).waitFor();
|
|
136
|
+
|
|
137
|
+
await page.evaluate(() => {
|
|
138
|
+
const app = document.querySelector("iframe").contentWindow;
|
|
139
|
+
app.postMessage({ jsonrpc: "2.0", method: "ui/notifications/host-context-changed", params: { theme: "dark" } }, "*");
|
|
140
|
+
app.postMessage({ jsonrpc: "2.0", method: "ui/notifications/tool-result", params: { structuredContent: {
|
|
141
|
+
title: "Spring peas",
|
|
142
|
+
matchingCount: 1,
|
|
143
|
+
varieties: [
|
|
144
|
+
{ name: "Meadow Sweet", growthHabit: "bush", peaType: "snap" },
|
|
145
|
+
],
|
|
146
|
+
notice: "No report was sent or stored.",
|
|
147
|
+
} } }, "*");
|
|
148
|
+
});
|
|
149
|
+
await status.filter({ hasText: "Preview updated: 1 sample pea variety matches." }).waitFor();
|
|
150
|
+
assert.equal(await frame.locator("html").getAttribute("data-emseepea-theme"), "dark");
|
|
151
|
+
assert.equal(await frame.evaluate((node) => node === document.querySelector("[data-emseepea-part='status']"), statusNode), true);
|
|
152
|
+
|
|
153
|
+
await frame.locator("button").press("Enter");
|
|
154
|
+
await status.filter({ hasText: "Asked for growing tips in the chat." }).waitFor();
|
|
155
|
+
assert.equal(await frame.locator("[data-emseepea-part='status']").evaluate((element) => element === document.activeElement), true);
|
|
156
|
+
const sent = await page.evaluate(() => window.receivedAppMessages.find((message) => message?.method === "ui/message"));
|
|
157
|
+
assert.equal(sent.params.content[0].text, "Show me growing tips for these pea varieties.");
|
|
158
|
+
|
|
159
|
+
await page.evaluate(() => document.querySelector("iframe").contentWindow.postMessage({
|
|
160
|
+
jsonrpc: "2.0", id: "teardown", method: "ui/resource-teardown",
|
|
161
|
+
}, "*"));
|
|
162
|
+
await status.filter({ hasText: "The preview was cancelled." }).waitFor();
|
|
163
|
+
const teardown = await page.evaluate(() => window.receivedAppMessages.find((message) => message?.id === "teardown"));
|
|
164
|
+
assert.deepEqual(teardown, { jsonrpc: "2.0", id: "teardown", result: {} });
|
|
100
165
|
});
|
|
101
166
|
|
|
102
167
|
test("the same UI template composes protected access and observability", async (t) => {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/create-react-ui-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.27",
|
|
4
4
|
"description": "Create an Em See Pea server with an accessible React form.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"starterDependencies": ["@emseepea/react", "@emseepea/server", "@emseepea/tailwind", "react", "react-dom"],
|
|
8
8
|
"scripts": {
|
|
9
9
|
"build": "npm run build:example && npm run build:initializer",
|
|
10
|
-
"build:example": "tsc -p tsconfig.json && esbuild src/client.tsx --bundle --minify --format=esm --platform=browser --outfile=dist/client.js",
|
|
10
|
+
"build:example": "tsc -p tsconfig.json && esbuild src/client.tsx --bundle --minify --format=esm --platform=browser --outfile=dist/client.js && esbuild src/mcp-app.tsx --bundle --minify --format=esm --platform=browser --outfile=dist/mcp-app-client.js",
|
|
11
11
|
"build:initializer": "node ../../scripts/build-initializer.mjs",
|
|
12
12
|
"start": "node dist/server.js",
|
|
13
13
|
"test": "npm run build && npm run test:built",
|
|
@@ -18,16 +18,16 @@
|
|
|
18
18
|
"prepack": "npm run build:initializer"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@emseepea/feedback": "0.2.
|
|
22
|
-
"@emseepea/react": "0.0
|
|
23
|
-
"@emseepea/server": "0.
|
|
21
|
+
"@emseepea/feedback": "0.2.10",
|
|
22
|
+
"@emseepea/react": "0.1.0",
|
|
23
|
+
"@emseepea/server": "0.11.0",
|
|
24
24
|
"@emseepea/tailwind": "0.0.2",
|
|
25
25
|
"react": "19.2.8",
|
|
26
26
|
"react-dom": "19.2.8",
|
|
27
27
|
"@emseepea/example-ui-shared": "0.0.0",
|
|
28
28
|
"playwright": "1.62.1",
|
|
29
29
|
"axe-core": "4.13.0",
|
|
30
|
-
"@emseepea/testing": "0.9.
|
|
30
|
+
"@emseepea/testing": "0.9.14",
|
|
31
31
|
"@types/node": "24.13.3",
|
|
32
32
|
"@types/react": "19.2.18",
|
|
33
33
|
"@types/react-dom": "19.2.5",
|