@emseepea/create-react-ui-server 0.0.24 → 0.0.26
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 +4 -4
- package/initializer-dist/template/src/capabilities/resource.pea-planting-plan-app.ts +74 -0
- package/initializer-dist/template/src/capabilities/tool.preview-planting-plan.ts +1 -1
- package/initializer-dist/template/src/ui-shared.tsx +10 -1
- package/initializer-dist/template/test/server.test.mjs +64 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -68,6 +68,10 @@ Open
|
|
|
68
68
|
`http://127.0.0.1:3001/` for the page or use
|
|
69
69
|
`http://127.0.0.1:3001/mcp` for Model Context Protocol (MCP).
|
|
70
70
|
|
|
71
|
+
The MCP endpoint also publishes an opt-in MCP Apps result card for the preview
|
|
72
|
+
tool. See the [UI example guide](https://emseepea.github.io/emseepea/examples/#add-a-web-form-or-mcp-app)
|
|
73
|
+
for the resource contract and ChatGPT compatibility fields.
|
|
74
|
+
|
|
71
75
|
The page previews a sample pea planting plan only. It does not send or store a report.
|
|
72
76
|
|
|
73
77
|
## Build a production container
|
|
@@ -50,6 +50,10 @@ Open
|
|
|
50
50
|
`http://127.0.0.1:3001/` for the page or use
|
|
51
51
|
`http://127.0.0.1:3001/mcp` for Model Context Protocol (MCP).
|
|
52
52
|
|
|
53
|
+
The MCP endpoint also publishes an opt-in MCP Apps result card for the preview
|
|
54
|
+
tool. See the [UI example guide](https://emseepea.github.io/emseepea/examples/#add-a-web-form-or-mcp-app)
|
|
55
|
+
for the resource contract and ChatGPT compatibility fields.
|
|
56
|
+
|
|
53
57
|
The page previews a sample pea planting plan only. It does not send or store a report.
|
|
54
58
|
|
|
55
59
|
## Build a production container
|
|
@@ -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.9",
|
|
19
19
|
"playwright": "1.62.1",
|
|
20
20
|
"axe-core": "4.13.0",
|
|
21
|
-
"@emseepea/testing": "0.9.
|
|
21
|
+
"@emseepea/testing": "0.9.13",
|
|
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.10.
|
|
34
|
+
"@emseepea/react": "0.0.22",
|
|
35
|
+
"@emseepea/server": "0.10.3",
|
|
36
36
|
"@emseepea/tailwind": "0.0.2",
|
|
37
37
|
"react": "19.2.8",
|
|
38
38
|
"react-dom": "19.2.8"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { plantingPlanAppResourceUri } from "../ui-shared.js";
|
|
2
|
+
import { defineResource, type AccessPolicy, type CapabilityModuleFactory } from "@emseepea/server";
|
|
3
|
+
|
|
4
|
+
const csp = { connectDomains: [], resourceDomains: [] };
|
|
5
|
+
const legacyCsp = { connect_domains: [], resource_domains: [] };
|
|
6
|
+
|
|
7
|
+
export default ((access) => defineResource({
|
|
8
|
+
name: "pea-planting-plan-app",
|
|
9
|
+
...access,
|
|
10
|
+
uri: plantingPlanAppResourceUri,
|
|
11
|
+
title: "Pea planting plan result",
|
|
12
|
+
description: "An accessible result card for the pea planting-plan preview tool.",
|
|
13
|
+
mimeType: "text/html;profile=mcp-app",
|
|
14
|
+
handler: () => ({
|
|
15
|
+
contents: [{
|
|
16
|
+
uri: plantingPlanAppResourceUri,
|
|
17
|
+
mimeType: "text/html;profile=mcp-app",
|
|
18
|
+
text: appHtml,
|
|
19
|
+
_meta: {
|
|
20
|
+
ui: { prefersBorder: true, csp },
|
|
21
|
+
"openai/widgetPrefersBorder": true,
|
|
22
|
+
"openai/widgetCSP": legacyCsp,
|
|
23
|
+
},
|
|
24
|
+
}],
|
|
25
|
+
}),
|
|
26
|
+
})) satisfies CapabilityModuleFactory<AccessPolicy>;
|
|
27
|
+
|
|
28
|
+
const appHtml = `<!doctype html>
|
|
29
|
+
<html lang="en">
|
|
30
|
+
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Pea planting plan result</title></head>
|
|
31
|
+
<body><main><h1>Pea planting plan result</h1><p id="status" role="status" aria-live="polite" aria-atomic="true">Waiting for the preview result.</p><ul id="varieties"></ul></main>
|
|
32
|
+
<script>
|
|
33
|
+
const status = document.querySelector("#status");
|
|
34
|
+
const list = document.querySelector("#varieties");
|
|
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>`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { createPreviewPlantingPlanTool } from "../ui-shared.js";
|
|
2
2
|
import type { AccessPolicy, CapabilityModuleFactory } from "@emseepea/server";
|
|
3
3
|
|
|
4
|
-
export default ((access) => createPreviewPlantingPlanTool(access)) satisfies CapabilityModuleFactory<AccessPolicy>;
|
|
4
|
+
export default ((access) => createPreviewPlantingPlanTool(access, true)) satisfies CapabilityModuleFactory<AccessPolicy>;
|
|
@@ -27,6 +27,8 @@ const outputSchema = z.strictObject({
|
|
|
27
27
|
notice: z.literal("No report was sent or stored.").describe("Reminder that the preview caused no external effect."),
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
export const plantingPlanAppResourceUri = "ui://pea-planting-plan/v1.html";
|
|
31
|
+
|
|
30
32
|
const varieties = [
|
|
31
33
|
{ name: "Harbour Gem", growthHabit: "bush" as const, peaType: "shelling" as const, tips: ["compact", "harvest when pods feel full"] },
|
|
32
34
|
{ name: "Highland Snap", growthHabit: "climbing" as const, peaType: "snap" as const, tips: ["provide support", "pick pods young"] },
|
|
@@ -45,12 +47,19 @@ export function previewPlantingPlan(input: z.output<typeof inputSchema>) {
|
|
|
45
47
|
};
|
|
46
48
|
}
|
|
47
49
|
|
|
48
|
-
export function createPreviewPlantingPlanTool(
|
|
50
|
+
export function createPreviewPlantingPlanTool(
|
|
51
|
+
access: AccessPolicy = { access: "public" },
|
|
52
|
+
appResource = false,
|
|
53
|
+
) {
|
|
49
54
|
return defineTool({
|
|
50
55
|
name: "preview-planting-plan",
|
|
51
56
|
...access,
|
|
52
57
|
title: "Preview a Pea Planting Plan",
|
|
53
58
|
description: "Preview a sample pea planting plan without sending, storing, or changing anything.",
|
|
59
|
+
...(appResource ? { _meta: {
|
|
60
|
+
ui: { resourceUri: plantingPlanAppResourceUri },
|
|
61
|
+
"openai/outputTemplate": plantingPlanAppResourceUri,
|
|
62
|
+
} } : {}),
|
|
54
63
|
inputSchema,
|
|
55
64
|
outputSchema,
|
|
56
65
|
handler: (input) => ({ data: previewPlantingPlan(input) }),
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
startEmseepea,
|
|
7
7
|
startMcpServer,
|
|
8
8
|
} from "@emseepea/testing";
|
|
9
|
+
import { chromium } from "playwright";
|
|
9
10
|
import { createReactUiServer } from "../dist/app.js";
|
|
10
11
|
|
|
11
12
|
test("describes every planting-plan tool property", async (t) => {
|
|
@@ -35,6 +36,69 @@ test("describes every planting-plan tool property", async (t) => {
|
|
|
35
36
|
assert.equal(result.content[0].text, JSON.stringify(result.structuredContent));
|
|
36
37
|
});
|
|
37
38
|
|
|
39
|
+
test("publishes the standards-first MCP Apps resource contract", async (t) => {
|
|
40
|
+
const running = await startMcpServer(t, new URL("../dist/server.js", import.meta.url));
|
|
41
|
+
const client = await running.connect();
|
|
42
|
+
const [tool] = (await client.listTools()).tools;
|
|
43
|
+
assert.deepEqual(tool._meta.ui, { resourceUri: "ui://pea-planting-plan/v1.html" });
|
|
44
|
+
assert.equal(tool._meta["openai/outputTemplate"], "ui://pea-planting-plan/v1.html");
|
|
45
|
+
|
|
46
|
+
const [resource] = (await client.listResources()).resources;
|
|
47
|
+
assert.equal(resource.uri, "ui://pea-planting-plan/v1.html");
|
|
48
|
+
assert.equal(resource.mimeType, "text/html;profile=mcp-app");
|
|
49
|
+
const [content] = (await client.readResource({ uri: resource.uri })).contents;
|
|
50
|
+
assert.equal(content.mimeType, "text/html;profile=mcp-app");
|
|
51
|
+
assert.deepEqual(content._meta.ui, {
|
|
52
|
+
prefersBorder: true,
|
|
53
|
+
csp: { connectDomains: [], resourceDomains: [] },
|
|
54
|
+
});
|
|
55
|
+
assert.deepEqual(content._meta["openai/widgetCSP"], {
|
|
56
|
+
connect_domains: [],
|
|
57
|
+
resource_domains: [],
|
|
58
|
+
});
|
|
59
|
+
assert.match(content.text, /method: "ui\/initialize"/);
|
|
60
|
+
assert.match(content.text, /method: "ui\/notifications\/initialized"/);
|
|
61
|
+
assert.match(content.text, /ui\/notifications\/tool-result/);
|
|
62
|
+
assert.match(content.text, /ui\/notifications\/tool-cancelled/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("the MCP Apps card completes initialization before rendering a result", async (t) => {
|
|
66
|
+
const running = await startMcpServer(t, new URL("../dist/server.js", import.meta.url));
|
|
67
|
+
const client = await running.connect();
|
|
68
|
+
const [resource] = (await client.listResources()).resources;
|
|
69
|
+
const [content] = (await client.readResource({ uri: resource.uri })).contents;
|
|
70
|
+
const browser = await chromium.launch({ headless: true });
|
|
71
|
+
t.after(() => browser.close());
|
|
72
|
+
const page = await browser.newPage();
|
|
73
|
+
await page.setContent("<iframe title=\"Pea planting plan result\"></iframe>");
|
|
74
|
+
await page.evaluate(() => {
|
|
75
|
+
window.addEventListener("message", (event) => {
|
|
76
|
+
if (event.data?.method === "ui/initialize") {
|
|
77
|
+
event.source.postMessage({ jsonrpc: "2.0", id: event.data.id, result: {
|
|
78
|
+
protocolVersion: "2026-01-26",
|
|
79
|
+
hostInfo: { name: "test-host", version: "1.0.0" },
|
|
80
|
+
hostCapabilities: {},
|
|
81
|
+
hostContext: {},
|
|
82
|
+
} }, "*");
|
|
83
|
+
}
|
|
84
|
+
if (event.data?.method === "ui/notifications/initialized") {
|
|
85
|
+
event.source.postMessage({
|
|
86
|
+
jsonrpc: "2.0",
|
|
87
|
+
method: "ui/notifications/tool-result",
|
|
88
|
+
params: { structuredContent: {
|
|
89
|
+
matchingCount: 1,
|
|
90
|
+
varieties: [{ name: "Highland Snap" }],
|
|
91
|
+
} },
|
|
92
|
+
}, "*");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
const frame = page.frames()[1];
|
|
97
|
+
await frame.setContent(content.text);
|
|
98
|
+
await frame.waitForFunction(() => document.querySelector("#status")?.textContent === "1 sample variety matches.");
|
|
99
|
+
assert.equal(await frame.locator("li").textContent(), "Highland Snap");
|
|
100
|
+
});
|
|
101
|
+
|
|
38
102
|
test("the same UI template composes protected access and observability", async (t) => {
|
|
39
103
|
const events = [];
|
|
40
104
|
const permissions = ["plans:preview"];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emseepea/create-react-ui-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.26",
|
|
4
4
|
"description": "Create an Em See Pea server with an accessible React form.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -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.10.
|
|
21
|
+
"@emseepea/feedback": "0.2.9",
|
|
22
|
+
"@emseepea/react": "0.0.22",
|
|
23
|
+
"@emseepea/server": "0.10.3",
|
|
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.13",
|
|
31
31
|
"@types/node": "24.13.3",
|
|
32
32
|
"@types/react": "19.2.18",
|
|
33
33
|
"@types/react-dom": "19.2.5",
|