@noodleseed/agent-kit 0.20.0 → 0.21.0
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 +2 -2
- package/manifest.json +37 -27
- package/package.json +2 -2
- package/skills/claude-code/SKILL.md +4 -3
- package/skills/claude-code/examples/acme-bistro/README.md +1 -1
- package/skills/claude-code/examples/acme-tasks/README.md +8 -3
- package/skills/claude-code/examples/food-ordering/README.md +8 -0
- package/skills/claude-code/examples/food-ordering/src/server.ts +3 -1
- package/skills/claude-code/examples/hello/README.md +4 -0
- package/skills/claude-code/examples/hello/src/server.ts +2 -1
- package/skills/claude-code/examples/hello/test/server.test.ts +11 -0
- package/skills/claude-code/references/authoring-workflow.md +1 -1
- package/skills/claude-code/references/compile-errors.md +1 -0
- package/skills/claude-code/references/deploy-and-ops.md +5 -0
- package/skills/claude-code/references/embedded-assistant.md +160 -0
- package/skills/claude-code/references/sdk-surface.md +2 -2
- package/skills/claude-code/references/widgets-and-apps.md +31 -21
- package/skills/codex/SKILL.md +4 -3
- package/skills/codex/examples/acme-bistro/README.md +1 -1
- package/skills/codex/examples/acme-tasks/README.md +8 -3
- package/skills/codex/examples/food-ordering/README.md +8 -0
- package/skills/codex/examples/food-ordering/src/server.ts +3 -1
- package/skills/codex/examples/hello/README.md +4 -0
- package/skills/codex/examples/hello/src/server.ts +2 -1
- package/skills/codex/examples/hello/test/server.test.ts +11 -0
- package/skills/codex/references/authoring-workflow.md +1 -1
- package/skills/codex/references/compile-errors.md +1 -0
- package/skills/codex/references/deploy-and-ops.md +5 -0
- package/skills/codex/references/embedded-assistant.md +160 -0
- package/skills/codex/references/sdk-surface.md +2 -2
- package/skills/codex/references/widgets-and-apps.md +31 -21
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ into a project's `.agents/` and `.claude/` skill directories via `noodle agents
|
|
|
15
15
|
- `skills/claude-code/` — the Claude Code skill, same hierarchical tree.
|
|
16
16
|
- `manifest.json` — `{ packageVersion, files: [{ path, sha256, agentTarget }] }`. The CLI verifies every
|
|
17
17
|
file's sha256 against this manifest before writing it into a user's project; a mismatch refuses the
|
|
18
|
-
write.
|
|
18
|
+
write. The manifest and every generated skill file are available in the public source tree for inspection.
|
|
19
19
|
|
|
20
20
|
## How the CLI uses it
|
|
21
21
|
|
|
@@ -35,5 +35,5 @@ reference files.
|
|
|
35
35
|
## License
|
|
36
36
|
|
|
37
37
|
Apache-2.0. This package is generated and published from the
|
|
38
|
-
[`noodle-
|
|
38
|
+
[`noodle-core`](https://github.com/NoodleSeed-com/noodle-core) repository; the renderer is the
|
|
39
39
|
`@noodle-borg/agent-kit` workspace package.
|
package/manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "0.
|
|
2
|
+
"packageVersion": "0.21.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": "skills/codex/SKILL.md",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "09469929851e57fae24fc411a173d0e356aabacb3743beed81232d03e2cc44dd",
|
|
7
7
|
"agentTarget": "codex"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"path": "skills/codex/references/sdk-surface.md",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "0916b1f4df01053c424b46eeb4e8ee0910709d66b956c4d19da77d5abc4b046b",
|
|
12
12
|
"agentTarget": "codex"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
@@ -23,12 +23,17 @@
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"path": "skills/codex/references/compile-errors.md",
|
|
26
|
-
"sha256": "
|
|
26
|
+
"sha256": "1039e245421e8c128cf1a81cf7a3ce809b0149478917040abebc124ffcb60423",
|
|
27
27
|
"agentTarget": "codex"
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "skills/codex/references/authoring-workflow.md",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "b4ca23d7545c8e96dbfee6a4cf2235d4eadfb27302e453fe631031041a17a8c6",
|
|
32
|
+
"agentTarget": "codex"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"path": "skills/codex/references/embedded-assistant.md",
|
|
36
|
+
"sha256": "db1eec6f6eed10b6525efc721b22756ff769b0234358821c15deb45cd61b39fb",
|
|
32
37
|
"agentTarget": "codex"
|
|
33
38
|
},
|
|
34
39
|
{
|
|
@@ -43,7 +48,7 @@
|
|
|
43
48
|
},
|
|
44
49
|
{
|
|
45
50
|
"path": "skills/codex/references/widgets-and-apps.md",
|
|
46
|
-
"sha256": "
|
|
51
|
+
"sha256": "69ff229945465793e6c9ae10e5185fc3b5e59b9fb508f03a81ab8d2a12b39efc",
|
|
47
52
|
"agentTarget": "codex"
|
|
48
53
|
},
|
|
49
54
|
{
|
|
@@ -58,7 +63,7 @@
|
|
|
58
63
|
},
|
|
59
64
|
{
|
|
60
65
|
"path": "skills/codex/references/deploy-and-ops.md",
|
|
61
|
-
"sha256": "
|
|
66
|
+
"sha256": "1b359a78e76ad6a71003a3d235685019cd2e1e2bbabd95ec520761d9915a2259",
|
|
62
67
|
"agentTarget": "codex"
|
|
63
68
|
},
|
|
64
69
|
{
|
|
@@ -78,7 +83,7 @@
|
|
|
78
83
|
},
|
|
79
84
|
{
|
|
80
85
|
"path": "skills/codex/examples/acme-bistro/README.md",
|
|
81
|
-
"sha256": "
|
|
86
|
+
"sha256": "2bcf803ea2407479ddc70644a79ad503335880fd8439ed673b17c8d50e7cb59f",
|
|
82
87
|
"agentTarget": "codex"
|
|
83
88
|
},
|
|
84
89
|
{
|
|
@@ -193,7 +198,7 @@
|
|
|
193
198
|
},
|
|
194
199
|
{
|
|
195
200
|
"path": "skills/codex/examples/acme-tasks/README.md",
|
|
196
|
-
"sha256": "
|
|
201
|
+
"sha256": "e1d654d1a98b721ef7ce4f5d5d3d868611079ed4dbf2126b766018adb95d4194",
|
|
197
202
|
"agentTarget": "codex"
|
|
198
203
|
},
|
|
199
204
|
{
|
|
@@ -248,7 +253,7 @@
|
|
|
248
253
|
},
|
|
249
254
|
{
|
|
250
255
|
"path": "skills/codex/examples/food-ordering/README.md",
|
|
251
|
-
"sha256": "
|
|
256
|
+
"sha256": "4d42dfbaf46ee8003ae84b3ef79c9d2a8b716943b85e2e9329fa875007019524",
|
|
252
257
|
"agentTarget": "codex"
|
|
253
258
|
},
|
|
254
259
|
{
|
|
@@ -268,7 +273,7 @@
|
|
|
268
273
|
},
|
|
269
274
|
{
|
|
270
275
|
"path": "skills/codex/examples/food-ordering/src/server.ts",
|
|
271
|
-
"sha256": "
|
|
276
|
+
"sha256": "6e8c4c17e6f3a7867232880e1a8d0e45428ebfd3938282d6bec7d8542ae7d712",
|
|
272
277
|
"agentTarget": "codex"
|
|
273
278
|
},
|
|
274
279
|
{
|
|
@@ -298,7 +303,7 @@
|
|
|
298
303
|
},
|
|
299
304
|
{
|
|
300
305
|
"path": "skills/codex/examples/hello/README.md",
|
|
301
|
-
"sha256": "
|
|
306
|
+
"sha256": "c39b89f95c4ae1a2b78a71f42c00910507c32eaba628be36e5ec3ef32508aaa5",
|
|
302
307
|
"agentTarget": "codex"
|
|
303
308
|
},
|
|
304
309
|
{
|
|
@@ -313,12 +318,12 @@
|
|
|
313
318
|
},
|
|
314
319
|
{
|
|
315
320
|
"path": "skills/codex/examples/hello/src/server.ts",
|
|
316
|
-
"sha256": "
|
|
321
|
+
"sha256": "f5ac538d951adc4f18b2c14205f4f710e7920da2ea911444d52adeab8f801521",
|
|
317
322
|
"agentTarget": "codex"
|
|
318
323
|
},
|
|
319
324
|
{
|
|
320
325
|
"path": "skills/codex/examples/hello/test/server.test.ts",
|
|
321
|
-
"sha256": "
|
|
326
|
+
"sha256": "4de4932d95a920f4e5fd78fc58f3583285aab3c3a97a3c1417f8a863ee05aa98",
|
|
322
327
|
"agentTarget": "codex"
|
|
323
328
|
},
|
|
324
329
|
{
|
|
@@ -348,12 +353,12 @@
|
|
|
348
353
|
},
|
|
349
354
|
{
|
|
350
355
|
"path": "skills/claude-code/SKILL.md",
|
|
351
|
-
"sha256": "
|
|
356
|
+
"sha256": "e31b696b0bcf72de525e0c28ceba8c4d44bd2ce5c6522476a0c33ffc8cb6d677",
|
|
352
357
|
"agentTarget": "claude-code"
|
|
353
358
|
},
|
|
354
359
|
{
|
|
355
360
|
"path": "skills/claude-code/references/sdk-surface.md",
|
|
356
|
-
"sha256": "
|
|
361
|
+
"sha256": "0916b1f4df01053c424b46eeb4e8ee0910709d66b956c4d19da77d5abc4b046b",
|
|
357
362
|
"agentTarget": "claude-code"
|
|
358
363
|
},
|
|
359
364
|
{
|
|
@@ -368,12 +373,17 @@
|
|
|
368
373
|
},
|
|
369
374
|
{
|
|
370
375
|
"path": "skills/claude-code/references/compile-errors.md",
|
|
371
|
-
"sha256": "
|
|
376
|
+
"sha256": "1039e245421e8c128cf1a81cf7a3ce809b0149478917040abebc124ffcb60423",
|
|
372
377
|
"agentTarget": "claude-code"
|
|
373
378
|
},
|
|
374
379
|
{
|
|
375
380
|
"path": "skills/claude-code/references/authoring-workflow.md",
|
|
376
|
-
"sha256": "
|
|
381
|
+
"sha256": "b4ca23d7545c8e96dbfee6a4cf2235d4eadfb27302e453fe631031041a17a8c6",
|
|
382
|
+
"agentTarget": "claude-code"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
"path": "skills/claude-code/references/embedded-assistant.md",
|
|
386
|
+
"sha256": "db1eec6f6eed10b6525efc721b22756ff769b0234358821c15deb45cd61b39fb",
|
|
377
387
|
"agentTarget": "claude-code"
|
|
378
388
|
},
|
|
379
389
|
{
|
|
@@ -388,7 +398,7 @@
|
|
|
388
398
|
},
|
|
389
399
|
{
|
|
390
400
|
"path": "skills/claude-code/references/widgets-and-apps.md",
|
|
391
|
-
"sha256": "
|
|
401
|
+
"sha256": "69ff229945465793e6c9ae10e5185fc3b5e59b9fb508f03a81ab8d2a12b39efc",
|
|
392
402
|
"agentTarget": "claude-code"
|
|
393
403
|
},
|
|
394
404
|
{
|
|
@@ -403,7 +413,7 @@
|
|
|
403
413
|
},
|
|
404
414
|
{
|
|
405
415
|
"path": "skills/claude-code/references/deploy-and-ops.md",
|
|
406
|
-
"sha256": "
|
|
416
|
+
"sha256": "1b359a78e76ad6a71003a3d235685019cd2e1e2bbabd95ec520761d9915a2259",
|
|
407
417
|
"agentTarget": "claude-code"
|
|
408
418
|
},
|
|
409
419
|
{
|
|
@@ -423,7 +433,7 @@
|
|
|
423
433
|
},
|
|
424
434
|
{
|
|
425
435
|
"path": "skills/claude-code/examples/acme-bistro/README.md",
|
|
426
|
-
"sha256": "
|
|
436
|
+
"sha256": "2bcf803ea2407479ddc70644a79ad503335880fd8439ed673b17c8d50e7cb59f",
|
|
427
437
|
"agentTarget": "claude-code"
|
|
428
438
|
},
|
|
429
439
|
{
|
|
@@ -538,7 +548,7 @@
|
|
|
538
548
|
},
|
|
539
549
|
{
|
|
540
550
|
"path": "skills/claude-code/examples/acme-tasks/README.md",
|
|
541
|
-
"sha256": "
|
|
551
|
+
"sha256": "e1d654d1a98b721ef7ce4f5d5d3d868611079ed4dbf2126b766018adb95d4194",
|
|
542
552
|
"agentTarget": "claude-code"
|
|
543
553
|
},
|
|
544
554
|
{
|
|
@@ -593,7 +603,7 @@
|
|
|
593
603
|
},
|
|
594
604
|
{
|
|
595
605
|
"path": "skills/claude-code/examples/food-ordering/README.md",
|
|
596
|
-
"sha256": "
|
|
606
|
+
"sha256": "4d42dfbaf46ee8003ae84b3ef79c9d2a8b716943b85e2e9329fa875007019524",
|
|
597
607
|
"agentTarget": "claude-code"
|
|
598
608
|
},
|
|
599
609
|
{
|
|
@@ -613,7 +623,7 @@
|
|
|
613
623
|
},
|
|
614
624
|
{
|
|
615
625
|
"path": "skills/claude-code/examples/food-ordering/src/server.ts",
|
|
616
|
-
"sha256": "
|
|
626
|
+
"sha256": "6e8c4c17e6f3a7867232880e1a8d0e45428ebfd3938282d6bec7d8542ae7d712",
|
|
617
627
|
"agentTarget": "claude-code"
|
|
618
628
|
},
|
|
619
629
|
{
|
|
@@ -643,7 +653,7 @@
|
|
|
643
653
|
},
|
|
644
654
|
{
|
|
645
655
|
"path": "skills/claude-code/examples/hello/README.md",
|
|
646
|
-
"sha256": "
|
|
656
|
+
"sha256": "c39b89f95c4ae1a2b78a71f42c00910507c32eaba628be36e5ec3ef32508aaa5",
|
|
647
657
|
"agentTarget": "claude-code"
|
|
648
658
|
},
|
|
649
659
|
{
|
|
@@ -658,12 +668,12 @@
|
|
|
658
668
|
},
|
|
659
669
|
{
|
|
660
670
|
"path": "skills/claude-code/examples/hello/src/server.ts",
|
|
661
|
-
"sha256": "
|
|
671
|
+
"sha256": "f5ac538d951adc4f18b2c14205f4f710e7920da2ea911444d52adeab8f801521",
|
|
662
672
|
"agentTarget": "claude-code"
|
|
663
673
|
},
|
|
664
674
|
{
|
|
665
675
|
"path": "skills/claude-code/examples/hello/test/server.test.ts",
|
|
666
|
-
"sha256": "
|
|
676
|
+
"sha256": "4de4932d95a920f4e5fd78fc58f3583285aab3c3a97a3c1417f8a863ee05aa98",
|
|
667
677
|
"agentTarget": "claude-code"
|
|
668
678
|
},
|
|
669
679
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Self-checking, self-updating agent skills for the Noodle Seed CLI. Authored in this repo by @noodle-borg/agent-kit; this is the published, independently-versioned canonical skills artifact the CLI fetches and verifies.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
],
|
|
25
25
|
"repository": {
|
|
26
26
|
"type": "git",
|
|
27
|
-
"url": "https://github.com/NoodleSeed-com/noodle-
|
|
27
|
+
"url": "https://github.com/NoodleSeed-com/noodle-core",
|
|
28
28
|
"directory": "packages/agent-kit"
|
|
29
29
|
},
|
|
30
30
|
"author": "Noodle Seed",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: noodle-seed
|
|
3
3
|
description: Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI.
|
|
4
|
-
version: 0.
|
|
5
|
-
hash:
|
|
4
|
+
version: 0.21.0
|
|
5
|
+
hash: b4fc528d406e2149
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Noodle Seed
|
|
@@ -25,7 +25,7 @@ Before authoring, design the experience — the funnel/handoff boundary, tools,
|
|
|
25
25
|
4. **Repair** — fix each `error.errors[]` entry at its `path`, then re-run `noodle validate --json`; `noodle validate --fix-prompt` emits ready-to-apply repair prose. Never freeform re-edit (see `references/compile-errors.md`).
|
|
26
26
|
5. **Smoke** — `noodle test --json`: local compile plus a loopback MCP smoke.
|
|
27
27
|
6. **Prove real output** — `validate`/`test` prove a connector tool *compiles and registers*, not that its response mapping returns data. Set the secret at the scope your local `noodle dev` resolves (`noodle secrets set <NAME> --runtime local --scope org --org local --from-env <ENV>`; a secret set at the wrong scope leaves the loopback returning `-32600 "not found"` — see `references/connect-an-api.md`), then run a live read — `noodle tools call <read_tool> --args '{...}'` executes the connector against the real API in-process — and confirm the mapped fields are populated, not `undefined`, before trusting it. Only run a live write if it is safe/approved.
|
|
28
|
-
7. **Apps/widgets** — `noodle check --json` (add `--target chatgpt|claude`), then `noodle devtools
|
|
28
|
+
7. **Apps/widgets/embed** — `noodle check --json` (add `--target chatgpt|claude|embedded-assistant`), then `noodle devtools`; use `references/widgets-and-apps.md` for MCP Apps and `references/embedded-assistant.md` for a SaaS embed.
|
|
29
29
|
8. **Deploy** — `noodle deploy`; auth fails clean with `error.next` = `noodle login` (see `references/deploy-and-ops.md`).
|
|
30
30
|
9. **Wire into a host** — `noodle connect <codex|claude-code|chatgpt>` (prove it in a real host per `references/test-in-hosts.md`; debug symptoms with `references/troubleshooting.md`).
|
|
31
31
|
10. **Health** — `noodle metrics --agent-output`: a health verdict plus the exact next command per attention item.
|
|
@@ -39,6 +39,7 @@ Scan all of these during discovery; open in full the ones your task touches:
|
|
|
39
39
|
- `references/cli-commands.md` — every `noodle` command, grouped by area.
|
|
40
40
|
- `references/compile-errors.md` — fix `noodle validate` errors by code.
|
|
41
41
|
- `references/authoring-workflow.md` — input paths (scrape / OpenAPI import / user interview), the fit check, the validate→test→dev repair loop, connectors, and secrets/variables.
|
|
42
|
+
- `references/embedded-assistant.md` — HTTPS origins, managed model config, deploy-before-client sequencing, backend session exchange, browser mounting, and credential boundaries.
|
|
42
43
|
- `references/connect-an-api.md` — given an API key: secure it, probe the live API to learn the real shape, model the connector, and prove real output before building.
|
|
43
44
|
- `references/experience-design.md` — design the app experience before authoring: funnel/handoff boundary, grounding, two-users, display modes, and the wireframe/UX spec.
|
|
44
45
|
- `references/widgets-and-apps.md` — MCP Apps, React `view` widgets, the widget hook surface, output shaping, and CSP.
|
|
@@ -34,7 +34,7 @@ In another terminal:
|
|
|
34
34
|
```sh
|
|
35
35
|
noodle tools list
|
|
36
36
|
noodle tools call show_menu --args '{"customer":"Asha"}'
|
|
37
|
-
noodle tools call create_checkout --args '{"customer":"Asha","cartToken":"
|
|
37
|
+
noodle tools call create_checkout --args '{"customer":"Asha","cartToken":"cart_1","total":36}'
|
|
38
38
|
noodle check --target chatgpt
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -97,13 +97,18 @@ assistant: embeddedAssistant({
|
|
|
97
97
|
The assistant automatically inherits this server's existing `branding` block, so its name, accent,
|
|
98
98
|
light/dark surfaces, density, and radius match the `TaskList` widget without a second brand declaration.
|
|
99
99
|
|
|
100
|
+
Every assistant origin is exact and HTTPS-only. If the customer web app needs local browser testing, it
|
|
101
|
+
must serve itself over development HTTPS and add its `https://localhost:<port>` origin; `noodle dev` does
|
|
102
|
+
not provide TLS for that separate SaaS app.
|
|
103
|
+
|
|
100
104
|
The customer backend exchanges its authenticated user through `@noodleseed/assistant/server`; the browser
|
|
101
105
|
uses the Web Component or React wrapper and never receives the embed client or model secret. Validate with
|
|
102
106
|
`noodle check --target embedded-assistant`, then create the backend credential with
|
|
103
|
-
`noodle assistant clients create` after deployment.
|
|
107
|
+
`noodle assistant clients create` after deployment. Model URL/name/key values stay in Noodle managed config;
|
|
108
|
+
only the Noodle service URL and assistant client ID/secret belong in the authenticated customer backend.
|
|
104
109
|
|
|
105
|
-
Install the independently versioned embed SDK
|
|
106
|
-
|
|
110
|
+
Install the independently versioned embed SDK with the customer web application's existing package manager;
|
|
111
|
+
do not introduce a second lockfile.
|
|
107
112
|
|
|
108
113
|
This example has no connector secrets and does not include tokens, caller-key mechanisms, or
|
|
109
114
|
`.env.noodle` values. All tasks are fictional seed data.
|
|
@@ -21,6 +21,14 @@ private customer data.
|
|
|
21
21
|
| Handoff | `handoff.allowedDomains` allows only `https://orders.example.com` checkout URLs |
|
|
22
22
|
| Progressive enhancement | Non-Apps hosts still receive stores, featured items, and a readable fallback summary |
|
|
23
23
|
|
|
24
|
+
The example is intentionally richer than the generated starter, but each inline view still follows the
|
|
25
|
+
same default: one immediate purpose, one primary action, at most one subordinate action, and progressive
|
|
26
|
+
disclosure for the rest. Preview it at 280px before adding navigation or local CSS; loading, empty, stale,
|
|
27
|
+
error/retry, and success states must remain readable without nested vertical scrolling.
|
|
28
|
+
|
|
29
|
+
Like the comprehensive default `noodle init my-app` scaffold, this flagship keeps the server feature-rich
|
|
30
|
+
while making each individual widget view focused; server capability breadth and screen density are separate.
|
|
31
|
+
|
|
24
32
|
## Local Author Loop
|
|
25
33
|
|
|
26
34
|
```sh
|
|
@@ -151,7 +151,9 @@ const cartStateSchema = z.object({
|
|
|
151
151
|
customer: z.string(),
|
|
152
152
|
notes: z.string().optional(),
|
|
153
153
|
subtotal: z.number(),
|
|
154
|
-
|
|
154
|
+
// `.default()`/`.optional()` state fields are optional on write: a cart save that omits
|
|
155
|
+
// `status` still validates, and a fresh cart starts in `draft`.
|
|
156
|
+
status: z.enum(['draft', 'review', 'handoff']).default('draft'),
|
|
155
157
|
checkoutUrl: z.string().optional(),
|
|
156
158
|
});
|
|
157
159
|
|
|
@@ -10,4 +10,8 @@ noodle dev examples/hello/src/server.ts --app hello
|
|
|
10
10
|
noodle deploy examples/hello/src/server.ts --org acme --app hello
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
`noodle export manifest examples/hello/src/server.ts` compiles the same entrypoint locally and prints
|
|
14
|
+
the portable, vendor-neutral manifest JSON — the eject path: your `server.ts` plus this manifest is
|
|
15
|
+
the whole app, yours to read, diff, and keep.
|
|
16
|
+
|
|
13
17
|
It is also the fixture for `pnpm smoke:dev` and the e2e harness, so keep its tool surface stable.
|
|
@@ -16,7 +16,8 @@ export default server(
|
|
|
16
16
|
tool('greet', {
|
|
17
17
|
description: 'Greet someone by name.',
|
|
18
18
|
input: z.object({
|
|
19
|
-
|
|
19
|
+
// Defaults are advertised to the model and applied at runtime when the argument is omitted.
|
|
20
|
+
name: z.string().default('world'),
|
|
20
21
|
}),
|
|
21
22
|
output: z.object({
|
|
22
23
|
message: z.string(),
|
|
@@ -5,4 +5,15 @@ describe('hello example', () => {
|
|
|
5
5
|
it('exports a Noodle server definition', () => {
|
|
6
6
|
expect(typeof app.toManifest).toBe('function');
|
|
7
7
|
});
|
|
8
|
+
|
|
9
|
+
it('advertises the greet default and keeps the argument optional', async () => {
|
|
10
|
+
const manifest = await app.toManifest();
|
|
11
|
+
const greet = manifest.tools?.find((tool) => tool.name === 'greet');
|
|
12
|
+
const schema = greet?.inputSchema as {
|
|
13
|
+
properties?: { name?: { default?: unknown } };
|
|
14
|
+
required?: string[];
|
|
15
|
+
};
|
|
16
|
+
expect(schema.properties?.name?.default).toBe('world');
|
|
17
|
+
expect(schema.required ?? []).not.toContain('name');
|
|
18
|
+
});
|
|
8
19
|
});
|
|
@@ -200,7 +200,7 @@ Author managed config as `secret("NAME")` / `variable("NAME")` and operate it wi
|
|
|
200
200
|
|
|
201
201
|
## Embedded assistant
|
|
202
202
|
|
|
203
|
-
To place the same server tools inside
|
|
203
|
+
To place the same server tools inside a SaaS web app, declare `assistant: embeddedAssistant(...)` alongside the one server-level brand kit. Read `embedded-assistant.md` before integrating: it owns the HTTPS-origin rule, managed model configuration, required deploy-before-client sequence, customer-backend exchange, browser mount, and verification checklist.
|
|
204
204
|
|
|
205
205
|
## Boundaries
|
|
206
206
|
|
|
@@ -18,6 +18,7 @@ Run `noodle validate` (add `--json` for the machine-readable envelope, `--fix-pr
|
|
|
18
18
|
| `invalid_name` | Rename the identifier to match the allowed pattern (lowercase, no spaces/reserved characters) cited at `path`. |
|
|
19
19
|
| `duplicate_name` | Two tools/components share a name; give each a unique name at the cited `path`. |
|
|
20
20
|
| `unsupported_manifest_version` | Update the SDK/CLI so the emitted manifest version is supported; do not pin an old manifest shape. |
|
|
21
|
+
| `reserved_for_future_version` | The verb at `path` (e.g. `compute`, `elicit`) is reserved for a future core version; express the step with `use` (a connector operation) or `map` (a pure mapping) instead. |
|
|
21
22
|
| `invalid_operation_ref` | Fix the connector operation reference to `alias.operation` for an operation that exists on that connector. |
|
|
22
23
|
| `external_ref` | Remove the external/remote `$ref`; schemas must be self-contained — inline the definition instead of dereferencing a URL. |
|
|
23
24
|
| `invalid_schema_ref` | Correct the `$use` schema reference syntax at `path`; it does not name a resolvable local schema. |
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- Authenticate
|
|
6
6
|
- Link and target
|
|
7
7
|
- Deploy and inspect
|
|
8
|
+
- Eject path (portable manifest)
|
|
8
9
|
- Connect into a host
|
|
9
10
|
- Access modes
|
|
10
11
|
- Org and members
|
|
@@ -24,6 +25,10 @@
|
|
|
24
25
|
|
|
25
26
|
`noodle deploy` deploys the server. Then `noodle open` (latest URL), `noodle status`, `noodle inspect` (metadata, no secrets), `noodle smoke` (readiness diagnostics), and `noodle rollback <deploymentId>` to revert.
|
|
26
27
|
|
|
28
|
+
## Eject path (portable manifest)
|
|
29
|
+
|
|
30
|
+
`noodle export manifest [--output <file>]` compiles the entrypoint locally and emits the portable, vendor-neutral manifest JSON — no service, no login, no account. A Noodle app is just `src/server.ts` plus this manifest: the user can read it, diff it, and keep it.
|
|
31
|
+
|
|
27
32
|
## Connect into a host
|
|
28
33
|
|
|
29
34
|
Once deployed, register the server as a tool in a host with `noodle connect <host>` (`claude-code`, `codex`, `chatgpt`, `cursor`, `vscode`, `claude`, `inspector`) — it prints the exact config to paste.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Embedded assistant
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Architecture
|
|
6
|
+
- Author and validate
|
|
7
|
+
- Configure and deploy
|
|
8
|
+
- Access modes and customer auth
|
|
9
|
+
- Create the backend client
|
|
10
|
+
- Integrate the customer backend
|
|
11
|
+
- The session response
|
|
12
|
+
- Mount the browser component
|
|
13
|
+
- Toolchain requirements
|
|
14
|
+
- Verify the boundary
|
|
15
|
+
- Troubleshooting: symptom to diagnosis
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
The browser never receives a model key, assistant client secret, MCP token, or raw application session. The embedding SaaS authenticates its own user, its backend exchanges that verified identity through `@noodleseed/assistant/server`, and the browser receives only a short-lived assistant session.
|
|
20
|
+
|
|
21
|
+
Keep the two configuration sets separate:
|
|
22
|
+
|
|
23
|
+
| Owner | Values | Destination |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| Noodle deployment | `ASSISTANT_MODEL_BASE_URL`, `ASSISTANT_MODEL`, `ASSISTANT_MODEL_API_KEY` | `noodle variables set` / `noodle secrets set`; never the SaaS environment |
|
|
26
|
+
| SaaS backend | `NOODLE_SERVICE_URL`, `NOODLE_ASSISTANT_CLIENT_ID`, `NOODLE_ASSISTANT_CLIENT_SECRET` | Backend-only environment or secret manager; never browser code or `NEXT_PUBLIC_*`/equivalent variables |
|
|
27
|
+
|
|
28
|
+
## Author and validate
|
|
29
|
+
|
|
30
|
+
Use the same server tools in the embed; do not create a second tool set. Declare one server-level brand kit and an assistant configuration:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
branding: { name: "Acme", accent: "#3157D5" },
|
|
34
|
+
assistant: embeddedAssistant({
|
|
35
|
+
model: openAICompatible({
|
|
36
|
+
baseUrl: variable("ASSISTANT_MODEL_BASE_URL"),
|
|
37
|
+
model: variable("ASSISTANT_MODEL"),
|
|
38
|
+
apiKey: secret("ASSISTANT_MODEL_API_KEY"),
|
|
39
|
+
}),
|
|
40
|
+
allowedOrigins: ["http://localhost:3000", "https://app.example.com"],
|
|
41
|
+
layout: { mode: "floating", position: "bottom-right" },
|
|
42
|
+
}),
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`allowedOrigins` are exact origins: scheme, host, and optional port, with no path, trailing slash, or wildcard. Production origins must be HTTPS; plain HTTP is accepted only for loopback development origins (`http://localhost:<port>`, `http://127.0.0.1:<port>`). `noodle dev` serves the MCP project, not the embedding SaaS.
|
|
46
|
+
|
|
47
|
+
Run:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
noodle validate --json
|
|
51
|
+
noodle check --target embedded-assistant --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use `noodle commands --json` before proposing command flags; do not invent flags from memory.
|
|
55
|
+
|
|
56
|
+
## Configure and deploy
|
|
57
|
+
|
|
58
|
+
Local MCP authoring and tests need no account, but an external browser embed needs an active assistant-enabled deployment before a backend client can be created. Set the model values on the target org/app/env, then deploy:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
noodle variables set ASSISTANT_MODEL_BASE_URL --scope env --org <org> --app <app> --env <env> --value <https-model-base-url>
|
|
62
|
+
noodle variables set ASSISTANT_MODEL --scope env --org <org> --app <app> --env <env> --value <model>
|
|
63
|
+
noodle secrets set ASSISTANT_MODEL_API_KEY --scope env --org <org> --app <app> --env <env> --from-env ASSISTANT_MODEL_API_KEY
|
|
64
|
+
noodle deploy --org <org> --app <app> --env <env>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Do not put these model values in the embedding SaaS environment. A production deployment may omit a local origin; include a loopback origin only when local browser integration is required.
|
|
68
|
+
|
|
69
|
+
## Access modes and customer auth
|
|
70
|
+
|
|
71
|
+
Session exchange authenticates with the backend client credentials, so the embed works under any `--access` mode. Add `--access customers` only when verified end customers should also call the MCP endpoint directly. That mode requires `server.auth`; `noodle deploy` preflights the rule locally and fails with `server_auth_required` before contacting the service. Fix by adding auth to server options:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
auth: customerAuth.bridge({ verifyUrl: "https://app.example.com/api/auth/verify" }),
|
|
75
|
+
// or a concrete adapter: customerAuth.firebase({ projectId, apiKey })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Create the backend client
|
|
79
|
+
|
|
80
|
+
After the deployment is active:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
noodle assistant clients create --name web --org <org> --app <app> --env <env>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The CLI writes `{ clientId, clientSecret }` to a mode-`0600` file and prints only its path. Move the values into the SaaS backend secret manager without printing or committing them. Rotation invalidates the previous secret.
|
|
87
|
+
|
|
88
|
+
## Integrate the customer backend
|
|
89
|
+
|
|
90
|
+
Read the customer repository lockfile or `packageManager` field and install `@noodleseed/assistant` with that existing package manager; never introduce a second lockfile.
|
|
91
|
+
|
|
92
|
+
Create an authenticated same-origin backend route:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { createAssistantSession } from "@noodleseed/assistant/server";
|
|
96
|
+
|
|
97
|
+
export async function POST(request: Request) {
|
|
98
|
+
const user = await requireCurrentUser(request);
|
|
99
|
+
const { context } = await request.json();
|
|
100
|
+
const session = await createAssistantSession({
|
|
101
|
+
serviceUrl: process.env.NOODLE_SERVICE_URL!,
|
|
102
|
+
clientId: process.env.NOODLE_ASSISTANT_CLIENT_ID!,
|
|
103
|
+
clientSecret: process.env.NOODLE_ASSISTANT_CLIENT_SECRET!,
|
|
104
|
+
origin: process.env.PUBLIC_APP_ORIGIN!,
|
|
105
|
+
user: { id: user.id, email: user.email, roles: user.roles },
|
|
106
|
+
context,
|
|
107
|
+
});
|
|
108
|
+
return Response.json(session);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Authenticate before exchange. Source `origin` from trusted server configuration or strictly match the request origin against the same exact allowlist; never accept an arbitrary request header. Treat page context as untrusted model context, never authorization. Forward the helper response unchanged.
|
|
113
|
+
|
|
114
|
+
`serviceUrl` is the Noodle Seed control-plane base URL: the value `noodle assistant clients create` prints, also stored as `serviceUrl` in `deployment.json`. It is NOT the deployment MCP endpoint (`url`, which ends in `/v1/mcp` and rejects session exchange). Never probe or guess endpoints with real credentials.
|
|
115
|
+
|
|
116
|
+
## The session response
|
|
117
|
+
|
|
118
|
+
The exchange returns the versioned Embedded Assistant v1 contract. `token`, `expiresAt`, and `endpoints.turns` / `endpoints.toolConfirmations` (absolute URLs) are always present; `configuration` is optional theming data. Forward the body unchanged; the widget posts turns to `endpoints.turns` itself. Do not rebuild, filter, or rewrite the response.
|
|
119
|
+
|
|
120
|
+
## Mount the browser component
|
|
121
|
+
|
|
122
|
+
Use the React wrapper in React applications:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { NoodleAssistant } from "@noodleseed/assistant/react";
|
|
126
|
+
|
|
127
|
+
<NoodleAssistant sessionEndpoint="/api/assistant/session" theme="auto" />;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Or import the package root once and mount `<noodle-assistant session-endpoint="/api/assistant/session" theme="auto"></noodle-assistant>`. Mount only inside the authenticated application surface.
|
|
131
|
+
|
|
132
|
+
The component renders a custom element and must mount client-side. In a Next.js App Router tree, put the mount in a `"use client"` component; from a server component or the Pages Router, load it with `next/dynamic` and `ssr: false`.
|
|
133
|
+
|
|
134
|
+
## Toolchain requirements
|
|
135
|
+
|
|
136
|
+
- Node.js 20+ for `@noodleseed/assistant/server`.
|
|
137
|
+
- The package ships ESM and CommonJS with full export conditions; no bundler aliases, `transpilePackages`, or ambient type shims are needed. If resolution fails, the installed package version is outdated: update `@noodleseed/assistant` instead of adding workarounds.
|
|
138
|
+
- TypeScript `moduleResolution` `bundler` or `node16` recommended; classic `node` also resolves the `/react` and `/server` subpaths.
|
|
139
|
+
|
|
140
|
+
## Verify the boundary
|
|
141
|
+
|
|
142
|
+
- Signed-out session exchange returns `401`.
|
|
143
|
+
- The browser network/DOM/storage contains no client secret or model key.
|
|
144
|
+
- The local and production origins match `allowedOrigins` character-for-character.
|
|
145
|
+
- Read-only closed-world tools may auto-run; writes require confirmation.
|
|
146
|
+
- An expired turn re-exchanges once; confirmations never replay.
|
|
147
|
+
- Wrong-origin and malformed-origin requests fail closed.
|
|
148
|
+
|
|
149
|
+
## Troubleshooting: symptom to diagnosis
|
|
150
|
+
|
|
151
|
+
| Symptom | Diagnosis | Fix |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| Widget renders but no reply arrives and model usage stays zero | Turns are not reaching the service: outdated `@noodleseed/assistant` package, or the session response was rebuilt/filtered by the backend route | Update the package to the latest version; forward the session response unchanged |
|
|
154
|
+
| `assistant-error` with code `invalid_response` | The turn endpoint returned HTML or non-SSE content (auth redirect, proxy page) | Check the backend session route path and any middleware/rewrites on the embedding app |
|
|
155
|
+
| Build error `Package path ./react is not exported` | Outdated package version with import-only export conditions | Update `@noodleseed/assistant`; do not add webpack aliases or type shims |
|
|
156
|
+
| Deploy fails with `server_auth_required` | `--access customers` without `server.auth` | Add `customerAuth.bridge(...)` or an adapter to server options |
|
|
157
|
+
| Validate rejects an origin | Non-loopback HTTP origin in `allowedOrigins` | Use the exact HTTPS production origin; HTTP is only for `localhost`/`127.0.0.1` |
|
|
158
|
+
| Session exchange returns 404 | `serviceUrl` points at the deployment MCP endpoint | Use the control-plane service URL printed by `noodle assistant clients create` |
|
|
159
|
+
| Session exchange returns 403 `origin is not allowed` | Request origin differs from `allowedOrigins` character-for-character | Align the exact scheme/host/port on both sides and redeploy |
|
|
160
|
+
| Hydration or `HTMLElement is not defined` errors | The component mounted during server rendering | Mount client-only (`"use client"` or `next/dynamic` with `ssr: false`) |
|
|
@@ -38,7 +38,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
|
|
|
38
38
|
|
|
39
39
|
### Customer auth
|
|
40
40
|
|
|
41
|
-
- `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments.
|
|
41
|
+
- `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments. Firebase Web App fields are browser-visible configuration: use `variable(...)`, not `secret(...)`, and restrict the key in Firebase.
|
|
42
42
|
|
|
43
43
|
### Sessions
|
|
44
44
|
|
|
@@ -60,7 +60,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
|
|
|
60
60
|
- `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
|
|
61
61
|
- View metadata (`viewTitle`, `viewDescription`, `csp`, `domain`, `permissions`) belongs on the tool that renders it; `asset("./path")` packages local files.
|
|
62
62
|
- `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
|
|
63
|
-
- `state` defines durable widget state handles; `handoff` declares allowed external domains for safe host handoff.
|
|
63
|
+
- `state` defines durable widget state handles; handle schemas may use `.optional()`/`.default()` — defaulted fields are optional on write, so a save that omits them still validates. `handoff` declares allowed external domains for safe host handoff.
|
|
64
64
|
|
|
65
65
|
## Recipes
|
|
66
66
|
|
|
@@ -18,6 +18,12 @@ Use `tool(name, { description, input, output, fulfil, view })` for a model-visib
|
|
|
18
18
|
|
|
19
19
|
Generated widgets, official examples, and agent-authored MCP Apps must start with `@noodleseed/one/react` primitives and semantic tokens. Custom React/CSS or third-party components remain valid when the kit lacks the required behavior or the developer explicitly requests them.
|
|
20
20
|
|
|
21
|
+
`noodle init my-app` defaults to a comprehensive server—tools, resource, prompt, state contract, branding, handoff, widget, and embedded assistant—so begin by adapting that complete surface. Use `--template hello` only when the product explicitly needs the minimal server.
|
|
22
|
+
|
|
23
|
+
The default composition rule is: build the smallest useful conversational surface. Inline has one purpose, one primary action, and at most two visible actions. Use progressive disclosure or a later conversational turn for secondary detail; request fullscreen only when the user asks or the task genuinely needs it. Never use nested scrolling. At 280px and wider, the widget must remain one-column, readable, touch-safe, and free of horizontal overflow. Remove secondary chrome before shrinking essential content.
|
|
24
|
+
|
|
25
|
+
Every production widget handles loading, empty, partial, stale, error, retry, and success states. Prefill fields from known tool results and choose safe, reversible defaults; preserve the user’s work across rerenders, and never preselect a consequential action. Use public React primitives and branding tokens. Never author against `ns-*`, `nsr-*`, or example-local `--nw-*` classes/tokens; those are implementation details, not alternate design systems.
|
|
26
|
+
|
|
21
27
|
## React hook surface
|
|
22
28
|
|
|
23
29
|
Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
|
|
@@ -47,7 +53,7 @@ Author React. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react
|
|
|
47
53
|
|
|
48
54
|
```tsx
|
|
49
55
|
import type { ServerDefinition } from '@noodleseed/one';
|
|
50
|
-
import { generateHelpers } from '@noodleseed/one/react';
|
|
56
|
+
import { Action, ActionBar, AsyncBoundary, Feedback, Field, Flow, Frame, Region, Select, generateHelpers } from '@noodleseed/one/react';
|
|
51
57
|
|
|
52
58
|
// One call wires the typed host bridge; destructure only the hooks this view uses.
|
|
53
59
|
const { useToolInfo, useCallTool, useViewState, useOpenExternal } =
|
|
@@ -71,28 +77,32 @@ export default function OrderStatus() {
|
|
|
71
77
|
|
|
72
78
|
return (
|
|
73
79
|
// data-llm mirrors the visible state back to the model as text context.
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
<Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
|
|
81
|
+
<Flow variant="stack">
|
|
82
|
+
<AsyncBoundary state={placeOrder} loading="Placing order…" error={(error) => error.message}>
|
|
83
|
+
<Region title="Order" description="Choose one item for pickup.">
|
|
84
|
+
<Field label="Item">
|
|
85
|
+
<Select value={item} onChange={(event) => setItem(event.currentTarget.value)} options={[
|
|
86
|
+
{ value: 'falafel_wrap', label: 'Falafel Wrap' },
|
|
87
|
+
{ value: 'lentil_soup', label: 'Lentil Soup' },
|
|
88
|
+
{ value: 'mint_lemonade', label: 'Mint Lemonade' },
|
|
89
|
+
]} />
|
|
90
|
+
</Field>
|
|
91
|
+
</Region>
|
|
92
|
+
{confirmed?.status ? <Feedback status="success">{confirmed.status}</Feedback> : null}
|
|
93
|
+
<ActionBar>
|
|
94
|
+
<Action variant="primary" pending={placeOrder.isPending} pendingLabel="Placing…"
|
|
87
95
|
onClick={() => placeOrder.callTool({ customer: shown?.customer ?? 'Guest', item })}
|
|
88
96
|
>
|
|
89
|
-
|
|
90
|
-
</
|
|
91
|
-
<
|
|
92
|
-
<button type="button" onClick={() => openExternal(checkoutUrl)}>
|
|
97
|
+
Place order
|
|
98
|
+
</Action>
|
|
99
|
+
<Action onClick={() => openExternal(checkoutUrl)}>
|
|
93
100
|
Continue checkout
|
|
94
|
-
</
|
|
95
|
-
|
|
101
|
+
</Action>
|
|
102
|
+
</ActionBar>
|
|
103
|
+
</AsyncBoundary>
|
|
104
|
+
</Flow>
|
|
105
|
+
</Frame>
|
|
96
106
|
);
|
|
97
107
|
}
|
|
98
108
|
```
|
|
@@ -169,7 +179,7 @@ export default server(
|
|
|
169
179
|
A "ChatGPT App" is not a separate authoring surface — it is exactly this MCP Apps widget rendered by the ChatGPT host. From the same declaration you author three things:
|
|
170
180
|
|
|
171
181
|
- `domain` on the widget — one https origin per app (required for app-store submission, optional for dev-mode testing).
|
|
172
|
-
- `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review).
|
|
182
|
+
- `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review). These three lists are the complete CSP surface — there is no `base-uri` list.
|
|
173
183
|
- server `handoff.allowedDomains` — the external-link targets `useOpenExternal()` opens.
|
|
174
184
|
|
|
175
185
|
The compiler emits the rest automatically: the `openai/*` metadata (`openai/outputTemplate`, `openai/widgetCSP`, `openai/widgetDescription`) and ChatGPT’s `redirect_domains` (derived from `handoff.allowedDomains`). `window.openai` and Claude’s ext-apps bridge are auto-detected at startup, so the same widget renders in both Claude and ChatGPT with no host-specific code.
|
package/skills/codex/SKILL.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: noodle-seed
|
|
3
3
|
description: Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI.
|
|
4
|
-
version: 0.
|
|
5
|
-
hash:
|
|
4
|
+
version: 0.21.0
|
|
5
|
+
hash: cd25535e1d6dbf4f
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Noodle Seed
|
|
@@ -25,7 +25,7 @@ Before authoring, design the experience — the funnel/handoff boundary, tools,
|
|
|
25
25
|
4. **Repair** — fix each `error.errors[]` entry at its `path`, then re-run `noodle validate --json`; `noodle validate --fix-prompt` emits ready-to-apply repair prose. Never freeform re-edit (see `references/compile-errors.md`).
|
|
26
26
|
5. **Smoke** — `noodle test --json`: local compile plus a loopback MCP smoke.
|
|
27
27
|
6. **Prove real output** — `validate`/`test` prove a connector tool *compiles and registers*, not that its response mapping returns data. Set the secret at the scope your local `noodle dev` resolves (`noodle secrets set <NAME> --runtime local --scope org --org local --from-env <ENV>`; a secret set at the wrong scope leaves the loopback returning `-32600 "not found"` — see `references/connect-an-api.md`), then run a live read — `noodle tools call <read_tool> --args '{...}'` executes the connector against the real API in-process — and confirm the mapped fields are populated, not `undefined`, before trusting it. Only run a live write if it is safe/approved.
|
|
28
|
-
7. **Apps/widgets** — `noodle check --json` (add `--target chatgpt|claude`), then `noodle devtools
|
|
28
|
+
7. **Apps/widgets/embed** — `noodle check --json` (add `--target chatgpt|claude|embedded-assistant`), then `noodle devtools`; use `references/widgets-and-apps.md` for MCP Apps and `references/embedded-assistant.md` for a SaaS embed.
|
|
29
29
|
8. **Deploy** — `noodle deploy`; auth fails clean with `error.next` = `noodle login` (see `references/deploy-and-ops.md`).
|
|
30
30
|
9. **Wire into a host** — `noodle connect <codex|claude-code|chatgpt>` (prove it in a real host per `references/test-in-hosts.md`; debug symptoms with `references/troubleshooting.md`).
|
|
31
31
|
10. **Health** — `noodle metrics --agent-output`: a health verdict plus the exact next command per attention item.
|
|
@@ -39,6 +39,7 @@ Scan all of these during discovery; open in full the ones your task touches:
|
|
|
39
39
|
- `references/cli-commands.md` — every `noodle` command, grouped by area.
|
|
40
40
|
- `references/compile-errors.md` — fix `noodle validate` errors by code.
|
|
41
41
|
- `references/authoring-workflow.md` — input paths (scrape / OpenAPI import / user interview), the fit check, the validate→test→dev repair loop, connectors, and secrets/variables.
|
|
42
|
+
- `references/embedded-assistant.md` — HTTPS origins, managed model config, deploy-before-client sequencing, backend session exchange, browser mounting, and credential boundaries.
|
|
42
43
|
- `references/connect-an-api.md` — given an API key: secure it, probe the live API to learn the real shape, model the connector, and prove real output before building.
|
|
43
44
|
- `references/experience-design.md` — design the app experience before authoring: funnel/handoff boundary, grounding, two-users, display modes, and the wireframe/UX spec.
|
|
44
45
|
- `references/widgets-and-apps.md` — MCP Apps, React `view` widgets, the widget hook surface, output shaping, and CSP.
|
|
@@ -34,7 +34,7 @@ In another terminal:
|
|
|
34
34
|
```sh
|
|
35
35
|
noodle tools list
|
|
36
36
|
noodle tools call show_menu --args '{"customer":"Asha"}'
|
|
37
|
-
noodle tools call create_checkout --args '{"customer":"Asha","cartToken":"
|
|
37
|
+
noodle tools call create_checkout --args '{"customer":"Asha","cartToken":"cart_1","total":36}'
|
|
38
38
|
noodle check --target chatgpt
|
|
39
39
|
```
|
|
40
40
|
|
|
@@ -97,13 +97,18 @@ assistant: embeddedAssistant({
|
|
|
97
97
|
The assistant automatically inherits this server's existing `branding` block, so its name, accent,
|
|
98
98
|
light/dark surfaces, density, and radius match the `TaskList` widget without a second brand declaration.
|
|
99
99
|
|
|
100
|
+
Every assistant origin is exact and HTTPS-only. If the customer web app needs local browser testing, it
|
|
101
|
+
must serve itself over development HTTPS and add its `https://localhost:<port>` origin; `noodle dev` does
|
|
102
|
+
not provide TLS for that separate SaaS app.
|
|
103
|
+
|
|
100
104
|
The customer backend exchanges its authenticated user through `@noodleseed/assistant/server`; the browser
|
|
101
105
|
uses the Web Component or React wrapper and never receives the embed client or model secret. Validate with
|
|
102
106
|
`noodle check --target embedded-assistant`, then create the backend credential with
|
|
103
|
-
`noodle assistant clients create` after deployment.
|
|
107
|
+
`noodle assistant clients create` after deployment. Model URL/name/key values stay in Noodle managed config;
|
|
108
|
+
only the Noodle service URL and assistant client ID/secret belong in the authenticated customer backend.
|
|
104
109
|
|
|
105
|
-
Install the independently versioned embed SDK
|
|
106
|
-
|
|
110
|
+
Install the independently versioned embed SDK with the customer web application's existing package manager;
|
|
111
|
+
do not introduce a second lockfile.
|
|
107
112
|
|
|
108
113
|
This example has no connector secrets and does not include tokens, caller-key mechanisms, or
|
|
109
114
|
`.env.noodle` values. All tasks are fictional seed data.
|
|
@@ -21,6 +21,14 @@ private customer data.
|
|
|
21
21
|
| Handoff | `handoff.allowedDomains` allows only `https://orders.example.com` checkout URLs |
|
|
22
22
|
| Progressive enhancement | Non-Apps hosts still receive stores, featured items, and a readable fallback summary |
|
|
23
23
|
|
|
24
|
+
The example is intentionally richer than the generated starter, but each inline view still follows the
|
|
25
|
+
same default: one immediate purpose, one primary action, at most one subordinate action, and progressive
|
|
26
|
+
disclosure for the rest. Preview it at 280px before adding navigation or local CSS; loading, empty, stale,
|
|
27
|
+
error/retry, and success states must remain readable without nested vertical scrolling.
|
|
28
|
+
|
|
29
|
+
Like the comprehensive default `noodle init my-app` scaffold, this flagship keeps the server feature-rich
|
|
30
|
+
while making each individual widget view focused; server capability breadth and screen density are separate.
|
|
31
|
+
|
|
24
32
|
## Local Author Loop
|
|
25
33
|
|
|
26
34
|
```sh
|
|
@@ -151,7 +151,9 @@ const cartStateSchema = z.object({
|
|
|
151
151
|
customer: z.string(),
|
|
152
152
|
notes: z.string().optional(),
|
|
153
153
|
subtotal: z.number(),
|
|
154
|
-
|
|
154
|
+
// `.default()`/`.optional()` state fields are optional on write: a cart save that omits
|
|
155
|
+
// `status` still validates, and a fresh cart starts in `draft`.
|
|
156
|
+
status: z.enum(['draft', 'review', 'handoff']).default('draft'),
|
|
155
157
|
checkoutUrl: z.string().optional(),
|
|
156
158
|
});
|
|
157
159
|
|
|
@@ -10,4 +10,8 @@ noodle dev examples/hello/src/server.ts --app hello
|
|
|
10
10
|
noodle deploy examples/hello/src/server.ts --org acme --app hello
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
+
`noodle export manifest examples/hello/src/server.ts` compiles the same entrypoint locally and prints
|
|
14
|
+
the portable, vendor-neutral manifest JSON — the eject path: your `server.ts` plus this manifest is
|
|
15
|
+
the whole app, yours to read, diff, and keep.
|
|
16
|
+
|
|
13
17
|
It is also the fixture for `pnpm smoke:dev` and the e2e harness, so keep its tool surface stable.
|
|
@@ -16,7 +16,8 @@ export default server(
|
|
|
16
16
|
tool('greet', {
|
|
17
17
|
description: 'Greet someone by name.',
|
|
18
18
|
input: z.object({
|
|
19
|
-
|
|
19
|
+
// Defaults are advertised to the model and applied at runtime when the argument is omitted.
|
|
20
|
+
name: z.string().default('world'),
|
|
20
21
|
}),
|
|
21
22
|
output: z.object({
|
|
22
23
|
message: z.string(),
|
|
@@ -5,4 +5,15 @@ describe('hello example', () => {
|
|
|
5
5
|
it('exports a Noodle server definition', () => {
|
|
6
6
|
expect(typeof app.toManifest).toBe('function');
|
|
7
7
|
});
|
|
8
|
+
|
|
9
|
+
it('advertises the greet default and keeps the argument optional', async () => {
|
|
10
|
+
const manifest = await app.toManifest();
|
|
11
|
+
const greet = manifest.tools?.find((tool) => tool.name === 'greet');
|
|
12
|
+
const schema = greet?.inputSchema as {
|
|
13
|
+
properties?: { name?: { default?: unknown } };
|
|
14
|
+
required?: string[];
|
|
15
|
+
};
|
|
16
|
+
expect(schema.properties?.name?.default).toBe('world');
|
|
17
|
+
expect(schema.required ?? []).not.toContain('name');
|
|
18
|
+
});
|
|
8
19
|
});
|
|
@@ -200,7 +200,7 @@ Author managed config as `secret("NAME")` / `variable("NAME")` and operate it wi
|
|
|
200
200
|
|
|
201
201
|
## Embedded assistant
|
|
202
202
|
|
|
203
|
-
To place the same server tools inside
|
|
203
|
+
To place the same server tools inside a SaaS web app, declare `assistant: embeddedAssistant(...)` alongside the one server-level brand kit. Read `embedded-assistant.md` before integrating: it owns the HTTPS-origin rule, managed model configuration, required deploy-before-client sequence, customer-backend exchange, browser mount, and verification checklist.
|
|
204
204
|
|
|
205
205
|
## Boundaries
|
|
206
206
|
|
|
@@ -18,6 +18,7 @@ Run `noodle validate` (add `--json` for the machine-readable envelope, `--fix-pr
|
|
|
18
18
|
| `invalid_name` | Rename the identifier to match the allowed pattern (lowercase, no spaces/reserved characters) cited at `path`. |
|
|
19
19
|
| `duplicate_name` | Two tools/components share a name; give each a unique name at the cited `path`. |
|
|
20
20
|
| `unsupported_manifest_version` | Update the SDK/CLI so the emitted manifest version is supported; do not pin an old manifest shape. |
|
|
21
|
+
| `reserved_for_future_version` | The verb at `path` (e.g. `compute`, `elicit`) is reserved for a future core version; express the step with `use` (a connector operation) or `map` (a pure mapping) instead. |
|
|
21
22
|
| `invalid_operation_ref` | Fix the connector operation reference to `alias.operation` for an operation that exists on that connector. |
|
|
22
23
|
| `external_ref` | Remove the external/remote `$ref`; schemas must be self-contained — inline the definition instead of dereferencing a URL. |
|
|
23
24
|
| `invalid_schema_ref` | Correct the `$use` schema reference syntax at `path`; it does not name a resolvable local schema. |
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- Authenticate
|
|
6
6
|
- Link and target
|
|
7
7
|
- Deploy and inspect
|
|
8
|
+
- Eject path (portable manifest)
|
|
8
9
|
- Connect into a host
|
|
9
10
|
- Access modes
|
|
10
11
|
- Org and members
|
|
@@ -24,6 +25,10 @@
|
|
|
24
25
|
|
|
25
26
|
`noodle deploy` deploys the server. Then `noodle open` (latest URL), `noodle status`, `noodle inspect` (metadata, no secrets), `noodle smoke` (readiness diagnostics), and `noodle rollback <deploymentId>` to revert.
|
|
26
27
|
|
|
28
|
+
## Eject path (portable manifest)
|
|
29
|
+
|
|
30
|
+
`noodle export manifest [--output <file>]` compiles the entrypoint locally and emits the portable, vendor-neutral manifest JSON — no service, no login, no account. A Noodle app is just `src/server.ts` plus this manifest: the user can read it, diff it, and keep it.
|
|
31
|
+
|
|
27
32
|
## Connect into a host
|
|
28
33
|
|
|
29
34
|
Once deployed, register the server as a tool in a host with `noodle connect <host>` (`claude-code`, `codex`, `chatgpt`, `cursor`, `vscode`, `claude`, `inspector`) — it prints the exact config to paste.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Embedded assistant
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- Architecture
|
|
6
|
+
- Author and validate
|
|
7
|
+
- Configure and deploy
|
|
8
|
+
- Access modes and customer auth
|
|
9
|
+
- Create the backend client
|
|
10
|
+
- Integrate the customer backend
|
|
11
|
+
- The session response
|
|
12
|
+
- Mount the browser component
|
|
13
|
+
- Toolchain requirements
|
|
14
|
+
- Verify the boundary
|
|
15
|
+
- Troubleshooting: symptom to diagnosis
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
The browser never receives a model key, assistant client secret, MCP token, or raw application session. The embedding SaaS authenticates its own user, its backend exchanges that verified identity through `@noodleseed/assistant/server`, and the browser receives only a short-lived assistant session.
|
|
20
|
+
|
|
21
|
+
Keep the two configuration sets separate:
|
|
22
|
+
|
|
23
|
+
| Owner | Values | Destination |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| Noodle deployment | `ASSISTANT_MODEL_BASE_URL`, `ASSISTANT_MODEL`, `ASSISTANT_MODEL_API_KEY` | `noodle variables set` / `noodle secrets set`; never the SaaS environment |
|
|
26
|
+
| SaaS backend | `NOODLE_SERVICE_URL`, `NOODLE_ASSISTANT_CLIENT_ID`, `NOODLE_ASSISTANT_CLIENT_SECRET` | Backend-only environment or secret manager; never browser code or `NEXT_PUBLIC_*`/equivalent variables |
|
|
27
|
+
|
|
28
|
+
## Author and validate
|
|
29
|
+
|
|
30
|
+
Use the same server tools in the embed; do not create a second tool set. Declare one server-level brand kit and an assistant configuration:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
branding: { name: "Acme", accent: "#3157D5" },
|
|
34
|
+
assistant: embeddedAssistant({
|
|
35
|
+
model: openAICompatible({
|
|
36
|
+
baseUrl: variable("ASSISTANT_MODEL_BASE_URL"),
|
|
37
|
+
model: variable("ASSISTANT_MODEL"),
|
|
38
|
+
apiKey: secret("ASSISTANT_MODEL_API_KEY"),
|
|
39
|
+
}),
|
|
40
|
+
allowedOrigins: ["http://localhost:3000", "https://app.example.com"],
|
|
41
|
+
layout: { mode: "floating", position: "bottom-right" },
|
|
42
|
+
}),
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`allowedOrigins` are exact origins: scheme, host, and optional port, with no path, trailing slash, or wildcard. Production origins must be HTTPS; plain HTTP is accepted only for loopback development origins (`http://localhost:<port>`, `http://127.0.0.1:<port>`). `noodle dev` serves the MCP project, not the embedding SaaS.
|
|
46
|
+
|
|
47
|
+
Run:
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
noodle validate --json
|
|
51
|
+
noodle check --target embedded-assistant --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Use `noodle commands --json` before proposing command flags; do not invent flags from memory.
|
|
55
|
+
|
|
56
|
+
## Configure and deploy
|
|
57
|
+
|
|
58
|
+
Local MCP authoring and tests need no account, but an external browser embed needs an active assistant-enabled deployment before a backend client can be created. Set the model values on the target org/app/env, then deploy:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
noodle variables set ASSISTANT_MODEL_BASE_URL --scope env --org <org> --app <app> --env <env> --value <https-model-base-url>
|
|
62
|
+
noodle variables set ASSISTANT_MODEL --scope env --org <org> --app <app> --env <env> --value <model>
|
|
63
|
+
noodle secrets set ASSISTANT_MODEL_API_KEY --scope env --org <org> --app <app> --env <env> --from-env ASSISTANT_MODEL_API_KEY
|
|
64
|
+
noodle deploy --org <org> --app <app> --env <env>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Do not put these model values in the embedding SaaS environment. A production deployment may omit a local origin; include a loopback origin only when local browser integration is required.
|
|
68
|
+
|
|
69
|
+
## Access modes and customer auth
|
|
70
|
+
|
|
71
|
+
Session exchange authenticates with the backend client credentials, so the embed works under any `--access` mode. Add `--access customers` only when verified end customers should also call the MCP endpoint directly. That mode requires `server.auth`; `noodle deploy` preflights the rule locally and fails with `server_auth_required` before contacting the service. Fix by adding auth to server options:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
auth: customerAuth.bridge({ verifyUrl: "https://app.example.com/api/auth/verify" }),
|
|
75
|
+
// or a concrete adapter: customerAuth.firebase({ projectId, apiKey })
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Create the backend client
|
|
79
|
+
|
|
80
|
+
After the deployment is active:
|
|
81
|
+
|
|
82
|
+
```sh
|
|
83
|
+
noodle assistant clients create --name web --org <org> --app <app> --env <env>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The CLI writes `{ clientId, clientSecret }` to a mode-`0600` file and prints only its path. Move the values into the SaaS backend secret manager without printing or committing them. Rotation invalidates the previous secret.
|
|
87
|
+
|
|
88
|
+
## Integrate the customer backend
|
|
89
|
+
|
|
90
|
+
Read the customer repository lockfile or `packageManager` field and install `@noodleseed/assistant` with that existing package manager; never introduce a second lockfile.
|
|
91
|
+
|
|
92
|
+
Create an authenticated same-origin backend route:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { createAssistantSession } from "@noodleseed/assistant/server";
|
|
96
|
+
|
|
97
|
+
export async function POST(request: Request) {
|
|
98
|
+
const user = await requireCurrentUser(request);
|
|
99
|
+
const { context } = await request.json();
|
|
100
|
+
const session = await createAssistantSession({
|
|
101
|
+
serviceUrl: process.env.NOODLE_SERVICE_URL!,
|
|
102
|
+
clientId: process.env.NOODLE_ASSISTANT_CLIENT_ID!,
|
|
103
|
+
clientSecret: process.env.NOODLE_ASSISTANT_CLIENT_SECRET!,
|
|
104
|
+
origin: process.env.PUBLIC_APP_ORIGIN!,
|
|
105
|
+
user: { id: user.id, email: user.email, roles: user.roles },
|
|
106
|
+
context,
|
|
107
|
+
});
|
|
108
|
+
return Response.json(session);
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Authenticate before exchange. Source `origin` from trusted server configuration or strictly match the request origin against the same exact allowlist; never accept an arbitrary request header. Treat page context as untrusted model context, never authorization. Forward the helper response unchanged.
|
|
113
|
+
|
|
114
|
+
`serviceUrl` is the Noodle Seed control-plane base URL: the value `noodle assistant clients create` prints, also stored as `serviceUrl` in `deployment.json`. It is NOT the deployment MCP endpoint (`url`, which ends in `/v1/mcp` and rejects session exchange). Never probe or guess endpoints with real credentials.
|
|
115
|
+
|
|
116
|
+
## The session response
|
|
117
|
+
|
|
118
|
+
The exchange returns the versioned Embedded Assistant v1 contract. `token`, `expiresAt`, and `endpoints.turns` / `endpoints.toolConfirmations` (absolute URLs) are always present; `configuration` is optional theming data. Forward the body unchanged; the widget posts turns to `endpoints.turns` itself. Do not rebuild, filter, or rewrite the response.
|
|
119
|
+
|
|
120
|
+
## Mount the browser component
|
|
121
|
+
|
|
122
|
+
Use the React wrapper in React applications:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
import { NoodleAssistant } from "@noodleseed/assistant/react";
|
|
126
|
+
|
|
127
|
+
<NoodleAssistant sessionEndpoint="/api/assistant/session" theme="auto" />;
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Or import the package root once and mount `<noodle-assistant session-endpoint="/api/assistant/session" theme="auto"></noodle-assistant>`. Mount only inside the authenticated application surface.
|
|
131
|
+
|
|
132
|
+
The component renders a custom element and must mount client-side. In a Next.js App Router tree, put the mount in a `"use client"` component; from a server component or the Pages Router, load it with `next/dynamic` and `ssr: false`.
|
|
133
|
+
|
|
134
|
+
## Toolchain requirements
|
|
135
|
+
|
|
136
|
+
- Node.js 20+ for `@noodleseed/assistant/server`.
|
|
137
|
+
- The package ships ESM and CommonJS with full export conditions; no bundler aliases, `transpilePackages`, or ambient type shims are needed. If resolution fails, the installed package version is outdated: update `@noodleseed/assistant` instead of adding workarounds.
|
|
138
|
+
- TypeScript `moduleResolution` `bundler` or `node16` recommended; classic `node` also resolves the `/react` and `/server` subpaths.
|
|
139
|
+
|
|
140
|
+
## Verify the boundary
|
|
141
|
+
|
|
142
|
+
- Signed-out session exchange returns `401`.
|
|
143
|
+
- The browser network/DOM/storage contains no client secret or model key.
|
|
144
|
+
- The local and production origins match `allowedOrigins` character-for-character.
|
|
145
|
+
- Read-only closed-world tools may auto-run; writes require confirmation.
|
|
146
|
+
- An expired turn re-exchanges once; confirmations never replay.
|
|
147
|
+
- Wrong-origin and malformed-origin requests fail closed.
|
|
148
|
+
|
|
149
|
+
## Troubleshooting: symptom to diagnosis
|
|
150
|
+
|
|
151
|
+
| Symptom | Diagnosis | Fix |
|
|
152
|
+
| --- | --- | --- |
|
|
153
|
+
| Widget renders but no reply arrives and model usage stays zero | Turns are not reaching the service: outdated `@noodleseed/assistant` package, or the session response was rebuilt/filtered by the backend route | Update the package to the latest version; forward the session response unchanged |
|
|
154
|
+
| `assistant-error` with code `invalid_response` | The turn endpoint returned HTML or non-SSE content (auth redirect, proxy page) | Check the backend session route path and any middleware/rewrites on the embedding app |
|
|
155
|
+
| Build error `Package path ./react is not exported` | Outdated package version with import-only export conditions | Update `@noodleseed/assistant`; do not add webpack aliases or type shims |
|
|
156
|
+
| Deploy fails with `server_auth_required` | `--access customers` without `server.auth` | Add `customerAuth.bridge(...)` or an adapter to server options |
|
|
157
|
+
| Validate rejects an origin | Non-loopback HTTP origin in `allowedOrigins` | Use the exact HTTPS production origin; HTTP is only for `localhost`/`127.0.0.1` |
|
|
158
|
+
| Session exchange returns 404 | `serviceUrl` points at the deployment MCP endpoint | Use the control-plane service URL printed by `noodle assistant clients create` |
|
|
159
|
+
| Session exchange returns 403 `origin is not allowed` | Request origin differs from `allowedOrigins` character-for-character | Align the exact scheme/host/port on both sides and redeploy |
|
|
160
|
+
| Hydration or `HTMLElement is not defined` errors | The component mounted during server rendering | Mount client-only (`"use client"` or `next/dynamic` with `ssr: false`) |
|
|
@@ -38,7 +38,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
|
|
|
38
38
|
|
|
39
39
|
### Customer auth
|
|
40
40
|
|
|
41
|
-
- `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments.
|
|
41
|
+
- `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments. Firebase Web App fields are browser-visible configuration: use `variable(...)`, not `secret(...)`, and restrict the key in Firebase.
|
|
42
42
|
|
|
43
43
|
### Sessions
|
|
44
44
|
|
|
@@ -60,7 +60,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
|
|
|
60
60
|
- `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
|
|
61
61
|
- View metadata (`viewTitle`, `viewDescription`, `csp`, `domain`, `permissions`) belongs on the tool that renders it; `asset("./path")` packages local files.
|
|
62
62
|
- `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
|
|
63
|
-
- `state` defines durable widget state handles; `handoff` declares allowed external domains for safe host handoff.
|
|
63
|
+
- `state` defines durable widget state handles; handle schemas may use `.optional()`/`.default()` — defaulted fields are optional on write, so a save that omits them still validates. `handoff` declares allowed external domains for safe host handoff.
|
|
64
64
|
|
|
65
65
|
## Recipes
|
|
66
66
|
|
|
@@ -18,6 +18,12 @@ Use `tool(name, { description, input, output, fulfil, view })` for a model-visib
|
|
|
18
18
|
|
|
19
19
|
Generated widgets, official examples, and agent-authored MCP Apps must start with `@noodleseed/one/react` primitives and semantic tokens. Custom React/CSS or third-party components remain valid when the kit lacks the required behavior or the developer explicitly requests them.
|
|
20
20
|
|
|
21
|
+
`noodle init my-app` defaults to a comprehensive server—tools, resource, prompt, state contract, branding, handoff, widget, and embedded assistant—so begin by adapting that complete surface. Use `--template hello` only when the product explicitly needs the minimal server.
|
|
22
|
+
|
|
23
|
+
The default composition rule is: build the smallest useful conversational surface. Inline has one purpose, one primary action, and at most two visible actions. Use progressive disclosure or a later conversational turn for secondary detail; request fullscreen only when the user asks or the task genuinely needs it. Never use nested scrolling. At 280px and wider, the widget must remain one-column, readable, touch-safe, and free of horizontal overflow. Remove secondary chrome before shrinking essential content.
|
|
24
|
+
|
|
25
|
+
Every production widget handles loading, empty, partial, stale, error, retry, and success states. Prefill fields from known tool results and choose safe, reversible defaults; preserve the user’s work across rerenders, and never preselect a consequential action. Use public React primitives and branding tokens. Never author against `ns-*`, `nsr-*`, or example-local `--nw-*` classes/tokens; those are implementation details, not alternate design systems.
|
|
26
|
+
|
|
21
27
|
## React hook surface
|
|
22
28
|
|
|
23
29
|
Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
|
|
@@ -47,7 +53,7 @@ Author React. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react
|
|
|
47
53
|
|
|
48
54
|
```tsx
|
|
49
55
|
import type { ServerDefinition } from '@noodleseed/one';
|
|
50
|
-
import { generateHelpers } from '@noodleseed/one/react';
|
|
56
|
+
import { Action, ActionBar, AsyncBoundary, Feedback, Field, Flow, Frame, Region, Select, generateHelpers } from '@noodleseed/one/react';
|
|
51
57
|
|
|
52
58
|
// One call wires the typed host bridge; destructure only the hooks this view uses.
|
|
53
59
|
const { useToolInfo, useCallTool, useViewState, useOpenExternal } =
|
|
@@ -71,28 +77,32 @@ export default function OrderStatus() {
|
|
|
71
77
|
|
|
72
78
|
return (
|
|
73
79
|
// data-llm mirrors the visible state back to the model as text context.
|
|
74
|
-
<
|
|
75
|
-
<
|
|
76
|
-
<
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
<Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
|
|
81
|
+
<Flow variant="stack">
|
|
82
|
+
<AsyncBoundary state={placeOrder} loading="Placing order…" error={(error) => error.message}>
|
|
83
|
+
<Region title="Order" description="Choose one item for pickup.">
|
|
84
|
+
<Field label="Item">
|
|
85
|
+
<Select value={item} onChange={(event) => setItem(event.currentTarget.value)} options={[
|
|
86
|
+
{ value: 'falafel_wrap', label: 'Falafel Wrap' },
|
|
87
|
+
{ value: 'lentil_soup', label: 'Lentil Soup' },
|
|
88
|
+
{ value: 'mint_lemonade', label: 'Mint Lemonade' },
|
|
89
|
+
]} />
|
|
90
|
+
</Field>
|
|
91
|
+
</Region>
|
|
92
|
+
{confirmed?.status ? <Feedback status="success">{confirmed.status}</Feedback> : null}
|
|
93
|
+
<ActionBar>
|
|
94
|
+
<Action variant="primary" pending={placeOrder.isPending} pendingLabel="Placing…"
|
|
87
95
|
onClick={() => placeOrder.callTool({ customer: shown?.customer ?? 'Guest', item })}
|
|
88
96
|
>
|
|
89
|
-
|
|
90
|
-
</
|
|
91
|
-
<
|
|
92
|
-
<button type="button" onClick={() => openExternal(checkoutUrl)}>
|
|
97
|
+
Place order
|
|
98
|
+
</Action>
|
|
99
|
+
<Action onClick={() => openExternal(checkoutUrl)}>
|
|
93
100
|
Continue checkout
|
|
94
|
-
</
|
|
95
|
-
|
|
101
|
+
</Action>
|
|
102
|
+
</ActionBar>
|
|
103
|
+
</AsyncBoundary>
|
|
104
|
+
</Flow>
|
|
105
|
+
</Frame>
|
|
96
106
|
);
|
|
97
107
|
}
|
|
98
108
|
```
|
|
@@ -169,7 +179,7 @@ export default server(
|
|
|
169
179
|
A "ChatGPT App" is not a separate authoring surface — it is exactly this MCP Apps widget rendered by the ChatGPT host. From the same declaration you author three things:
|
|
170
180
|
|
|
171
181
|
- `domain` on the widget — one https origin per app (required for app-store submission, optional for dev-mode testing).
|
|
172
|
-
- `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review).
|
|
182
|
+
- `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review). These three lists are the complete CSP surface — there is no `base-uri` list.
|
|
173
183
|
- server `handoff.allowedDomains` — the external-link targets `useOpenExternal()` opens.
|
|
174
184
|
|
|
175
185
|
The compiler emits the rest automatically: the `openai/*` metadata (`openai/outputTemplate`, `openai/widgetCSP`, `openai/widgetDescription`) and ChatGPT’s `redirect_domains` (derived from `handoff.allowedDomains`). `window.openai` and Claude’s ext-apps bridge are auto-detected at startup, so the same widget renders in both Claude and ChatGPT with no host-specific code.
|