@noodleseed/agent-kit 0.15.0 → 0.17.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/manifest.json +21 -21
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +6 -4
- package/skills/claude-code/examples/food-ordering/src/server.ts +19 -19
- package/skills/claude-code/examples/weather/README.md +36 -5
- package/skills/claude-code/examples/weather/src/server.ts +88 -29
- package/skills/claude-code/examples/weather/test/server.test.ts +21 -0
- package/skills/claude-code/references/authoring-workflow.md +10 -10
- package/skills/claude-code/references/connect-an-api.md +63 -1
- package/skills/claude-code/references/examples.md +1 -1
- package/skills/claude-code/references/sdk-surface.md +6 -6
- package/skills/claude-code/references/troubleshooting.md +2 -0
- package/skills/codex/SKILL.md +6 -4
- package/skills/codex/examples/food-ordering/src/server.ts +19 -19
- package/skills/codex/examples/weather/README.md +36 -5
- package/skills/codex/examples/weather/src/server.ts +88 -29
- package/skills/codex/examples/weather/test/server.test.ts +21 -0
- package/skills/codex/references/authoring-workflow.md +10 -10
- package/skills/codex/references/connect-an-api.md +63 -1
- package/skills/codex/references/examples.md +1 -1
- package/skills/codex/references/sdk-surface.md +6 -6
- package/skills/codex/references/troubleshooting.md +2 -0
package/manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "0.
|
|
2
|
+
"packageVersion": "0.17.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": "skills/codex/SKILL.md",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "5f24dd83ca00ad0b078de17f0a978b28ee17b17678ab103a0151f550444e1c47",
|
|
7
7
|
"agentTarget": "codex"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"path": "skills/codex/references/sdk-surface.md",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "7275aedfb2338040f1b9f989765179ce970d1b25c77066b3d3f4f4e2fe39f7cc",
|
|
12
12
|
"agentTarget": "codex"
|
|
13
13
|
},
|
|
14
14
|
{
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "skills/codex/references/authoring-workflow.md",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "bf59d1550ce72fa188e50e74b7d586be49a0e5473501ee3b83a5f41687ac5163",
|
|
32
32
|
"agentTarget": "codex"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"path": "skills/codex/references/connect-an-api.md",
|
|
36
|
-
"sha256": "
|
|
36
|
+
"sha256": "497221256ebb78300b5e4055daff3f222052a7b78fb829fd24171668dd7fc0fa",
|
|
37
37
|
"agentTarget": "codex"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"path": "skills/codex/references/troubleshooting.md",
|
|
56
|
-
"sha256": "
|
|
56
|
+
"sha256": "b08dc48e72fa186fc953cf7052bfb7500b4586a772a1f4c54ff1e53a29f21eb2",
|
|
57
57
|
"agentTarget": "codex"
|
|
58
58
|
},
|
|
59
59
|
{
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
},
|
|
74
74
|
{
|
|
75
75
|
"path": "skills/codex/references/examples.md",
|
|
76
|
-
"sha256": "
|
|
76
|
+
"sha256": "bd19dead1f7b1b6610a53694a06569d3c71d149bd6c72c4a1c94c83ded13a34f",
|
|
77
77
|
"agentTarget": "codex"
|
|
78
78
|
},
|
|
79
79
|
{
|
|
@@ -268,7 +268,7 @@
|
|
|
268
268
|
},
|
|
269
269
|
{
|
|
270
270
|
"path": "skills/codex/examples/food-ordering/src/server.ts",
|
|
271
|
-
"sha256": "
|
|
271
|
+
"sha256": "491e7e7e8289d4295d09ca2a5f7035f00a06e1d9f3349c406397f90a966269df",
|
|
272
272
|
"agentTarget": "codex"
|
|
273
273
|
},
|
|
274
274
|
{
|
|
@@ -323,7 +323,7 @@
|
|
|
323
323
|
},
|
|
324
324
|
{
|
|
325
325
|
"path": "skills/codex/examples/weather/README.md",
|
|
326
|
-
"sha256": "
|
|
326
|
+
"sha256": "59dd103ae387b4ef0f32e155dbe9a11d79e260310a07c0f266a16add2e8e31bd",
|
|
327
327
|
"agentTarget": "codex"
|
|
328
328
|
},
|
|
329
329
|
{
|
|
@@ -338,22 +338,22 @@
|
|
|
338
338
|
},
|
|
339
339
|
{
|
|
340
340
|
"path": "skills/codex/examples/weather/src/server.ts",
|
|
341
|
-
"sha256": "
|
|
341
|
+
"sha256": "95cd3a2ef47be898469d40d109f3fb89ac6e59a34fd8ad600c80dfe2908d4598",
|
|
342
342
|
"agentTarget": "codex"
|
|
343
343
|
},
|
|
344
344
|
{
|
|
345
345
|
"path": "skills/codex/examples/weather/test/server.test.ts",
|
|
346
|
-
"sha256": "
|
|
346
|
+
"sha256": "6753e4db274b00d2e92405040a27609a15087227de7b13f9e9f9e19c12d3a255",
|
|
347
347
|
"agentTarget": "codex"
|
|
348
348
|
},
|
|
349
349
|
{
|
|
350
350
|
"path": "skills/claude-code/SKILL.md",
|
|
351
|
-
"sha256": "
|
|
351
|
+
"sha256": "24cdedbf4db8484b6e06823037b5b8fd0ef7ee39a57eb0fd135d167196fea3ff",
|
|
352
352
|
"agentTarget": "claude-code"
|
|
353
353
|
},
|
|
354
354
|
{
|
|
355
355
|
"path": "skills/claude-code/references/sdk-surface.md",
|
|
356
|
-
"sha256": "
|
|
356
|
+
"sha256": "7275aedfb2338040f1b9f989765179ce970d1b25c77066b3d3f4f4e2fe39f7cc",
|
|
357
357
|
"agentTarget": "claude-code"
|
|
358
358
|
},
|
|
359
359
|
{
|
|
@@ -373,12 +373,12 @@
|
|
|
373
373
|
},
|
|
374
374
|
{
|
|
375
375
|
"path": "skills/claude-code/references/authoring-workflow.md",
|
|
376
|
-
"sha256": "
|
|
376
|
+
"sha256": "bf59d1550ce72fa188e50e74b7d586be49a0e5473501ee3b83a5f41687ac5163",
|
|
377
377
|
"agentTarget": "claude-code"
|
|
378
378
|
},
|
|
379
379
|
{
|
|
380
380
|
"path": "skills/claude-code/references/connect-an-api.md",
|
|
381
|
-
"sha256": "
|
|
381
|
+
"sha256": "497221256ebb78300b5e4055daff3f222052a7b78fb829fd24171668dd7fc0fa",
|
|
382
382
|
"agentTarget": "claude-code"
|
|
383
383
|
},
|
|
384
384
|
{
|
|
@@ -398,7 +398,7 @@
|
|
|
398
398
|
},
|
|
399
399
|
{
|
|
400
400
|
"path": "skills/claude-code/references/troubleshooting.md",
|
|
401
|
-
"sha256": "
|
|
401
|
+
"sha256": "b08dc48e72fa186fc953cf7052bfb7500b4586a772a1f4c54ff1e53a29f21eb2",
|
|
402
402
|
"agentTarget": "claude-code"
|
|
403
403
|
},
|
|
404
404
|
{
|
|
@@ -418,7 +418,7 @@
|
|
|
418
418
|
},
|
|
419
419
|
{
|
|
420
420
|
"path": "skills/claude-code/references/examples.md",
|
|
421
|
-
"sha256": "
|
|
421
|
+
"sha256": "bd19dead1f7b1b6610a53694a06569d3c71d149bd6c72c4a1c94c83ded13a34f",
|
|
422
422
|
"agentTarget": "claude-code"
|
|
423
423
|
},
|
|
424
424
|
{
|
|
@@ -613,7 +613,7 @@
|
|
|
613
613
|
},
|
|
614
614
|
{
|
|
615
615
|
"path": "skills/claude-code/examples/food-ordering/src/server.ts",
|
|
616
|
-
"sha256": "
|
|
616
|
+
"sha256": "491e7e7e8289d4295d09ca2a5f7035f00a06e1d9f3349c406397f90a966269df",
|
|
617
617
|
"agentTarget": "claude-code"
|
|
618
618
|
},
|
|
619
619
|
{
|
|
@@ -668,7 +668,7 @@
|
|
|
668
668
|
},
|
|
669
669
|
{
|
|
670
670
|
"path": "skills/claude-code/examples/weather/README.md",
|
|
671
|
-
"sha256": "
|
|
671
|
+
"sha256": "59dd103ae387b4ef0f32e155dbe9a11d79e260310a07c0f266a16add2e8e31bd",
|
|
672
672
|
"agentTarget": "claude-code"
|
|
673
673
|
},
|
|
674
674
|
{
|
|
@@ -683,12 +683,12 @@
|
|
|
683
683
|
},
|
|
684
684
|
{
|
|
685
685
|
"path": "skills/claude-code/examples/weather/src/server.ts",
|
|
686
|
-
"sha256": "
|
|
686
|
+
"sha256": "95cd3a2ef47be898469d40d109f3fb89ac6e59a34fd8ad600c80dfe2908d4598",
|
|
687
687
|
"agentTarget": "claude-code"
|
|
688
688
|
},
|
|
689
689
|
{
|
|
690
690
|
"path": "skills/claude-code/examples/weather/test/server.test.ts",
|
|
691
|
-
"sha256": "
|
|
691
|
+
"sha256": "6753e4db274b00d2e92405040a27609a15087227de7b13f9e9f9e19c12d3a255",
|
|
692
692
|
"agentTarget": "claude-code"
|
|
693
693
|
}
|
|
694
694
|
]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -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.17.0
|
|
5
|
+
hash: b4952708b1c38f55
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Noodle Seed
|
|
@@ -15,6 +15,8 @@ Use this skill for project-local Noodle Seed authoring in Claude Code; preserve
|
|
|
15
15
|
|
|
16
16
|
This CLI is agent-native: the cold-agent-path commands speak the `--json` envelope (hosted admin/ops commands are still being normalized). Drive the loop by parsing machine state, not human prose. The full envelope, exit codes, and output modes are in `references/agent-contract.md`.
|
|
17
17
|
|
|
18
|
+
**Discover the skill first.** Before authoring anything, read this whole `SKILL.md` and scan the `## References` index below (including `references/examples.md`) so you build from the shipped patterns — connectors that return live lists, secret scoping, widgets, testing — instead of rediscovering them. Open the references your task touches in full.
|
|
19
|
+
|
|
18
20
|
Before authoring, design the experience — the funnel/handoff boundary, tools, widgets, display modes, and grounding — see `references/experience-design.md`. Then run the loop:
|
|
19
21
|
|
|
20
22
|
1. **Discover** — `noodle commands --json`: every command, subcommand, flag, and exit code (don't read source).
|
|
@@ -22,7 +24,7 @@ Before authoring, design the experience — the funnel/handoff boundary, tools,
|
|
|
22
24
|
3. **Validate** — `noodle validate --json`; on failure `{ok:false,error:{code,message,fix,next,errors:[{code,path,message}]}}` — the per-field detail is in `error.errors[]`.
|
|
23
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`).
|
|
24
26
|
5. **Smoke** — `noodle test --json`: local compile plus a loopback MCP smoke.
|
|
25
|
-
6. **Prove real output** — `validate`/`test` prove a connector tool *compiles and registers*, not that its response mapping returns data. Set the secret
|
|
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.
|
|
26
28
|
7. **Apps/widgets** — `noodle check --json` (add `--target chatgpt|claude`), then `noodle devtools` (see `references/widgets-and-apps.md`).
|
|
27
29
|
8. **Deploy** — `noodle deploy`; auth fails clean with `error.next` = `noodle login` (see `references/deploy-and-ops.md`).
|
|
28
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`).
|
|
@@ -30,7 +32,7 @@ Before authoring, design the experience — the funnel/handoff boundary, tools,
|
|
|
30
32
|
|
|
31
33
|
## References
|
|
32
34
|
|
|
33
|
-
|
|
35
|
+
Scan all of these during discovery; open in full the ones your task touches:
|
|
34
36
|
|
|
35
37
|
- `references/agent-contract.md` — the `--json` envelope, exit codes, and the three output modes.
|
|
36
38
|
- `references/sdk-surface.md` — what to import from `@noodleseed/one` and which builder to use.
|
|
@@ -17,28 +17,28 @@ const state = connector('noodle_state')
|
|
|
17
17
|
.version('1.0.0')
|
|
18
18
|
.operation('read_state', {
|
|
19
19
|
type: 'read',
|
|
20
|
-
input: {
|
|
21
|
-
handle:
|
|
22
|
-
key:
|
|
23
|
-
},
|
|
24
|
-
output: {
|
|
25
|
-
value:
|
|
26
|
-
revision:
|
|
27
|
-
status:
|
|
28
|
-
},
|
|
20
|
+
input: z.object({
|
|
21
|
+
handle: z.string(),
|
|
22
|
+
key: z.string().optional(),
|
|
23
|
+
}),
|
|
24
|
+
output: z.object({
|
|
25
|
+
value: z.record(z.string(), z.unknown()),
|
|
26
|
+
revision: z.number().int(),
|
|
27
|
+
status: z.string(),
|
|
28
|
+
}),
|
|
29
29
|
})
|
|
30
30
|
.operation('patch_state', {
|
|
31
31
|
type: 'action',
|
|
32
|
-
input: {
|
|
33
|
-
handle:
|
|
34
|
-
expectedRevision:
|
|
35
|
-
value:
|
|
36
|
-
},
|
|
37
|
-
output: {
|
|
38
|
-
value:
|
|
39
|
-
revision:
|
|
40
|
-
status:
|
|
41
|
-
},
|
|
32
|
+
input: z.object({
|
|
33
|
+
handle: z.string(),
|
|
34
|
+
expectedRevision: z.number().int(),
|
|
35
|
+
value: z.record(z.string(), z.unknown()),
|
|
36
|
+
}),
|
|
37
|
+
output: z.object({
|
|
38
|
+
value: z.record(z.string(), z.unknown()),
|
|
39
|
+
revision: z.number().int(),
|
|
40
|
+
status: z.string(),
|
|
41
|
+
}),
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
const stores = [
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
# Weather Briefing
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two declarative tools that show the runtime's breadth working together, with **no auth and no API
|
|
4
4
|
keys**. The `weather_briefing` tool takes a city name and runs a **three-step flow**:
|
|
5
5
|
|
|
6
|
-
Capability slots: HTTP connector authoring, ordered fulfilment flows, query/response mapping,
|
|
7
|
-
|
|
6
|
+
Capability slots: HTTP connector authoring, ordered fulfilment flows, query/response mapping,
|
|
7
|
+
**list-returning connector output** (a connector that returns a live, variable-length array), and
|
|
8
|
+
sandboxed compute.
|
|
8
9
|
|
|
9
10
|
1. **`geo.search`** → geocode the city to coordinates (Open-Meteo Geocoding API)
|
|
10
11
|
2. **`forecast.current`** → fetch current weather for those coordinates (Open-Meteo Forecast API)
|
|
11
12
|
3. **`brief.summarize`** → derive a human-readable briefing in a **WASM/QuickJS compute sandbox**
|
|
12
13
|
|
|
14
|
+
The second tool, `search_places`, shows a connector returning a **live, variable-length list**: it binds
|
|
15
|
+
the whole Open-Meteo geocoding `results` array with `${response.results}`, then narrows each match to
|
|
16
|
+
`{ id, label }` in a compute connector — the "search → a list of options the model can pick from"
|
|
17
|
+
pattern. Narrowing lives in compute because a `${...}` response mapping cannot iterate an array and a
|
|
18
|
+
tool's Zod output does not strip fields at runtime.
|
|
19
|
+
|
|
13
20
|
It exercises, in one TypeScript-authored app:
|
|
14
21
|
|
|
15
22
|
- **Server-level branding** with semantic tokens carried through the runtime artifact for any generated
|
|
@@ -17,8 +24,12 @@ It exercises, in one TypeScript-authored app:
|
|
|
17
24
|
- **Ordered flow execution** with outputs threaded between steps (`${steps.geo.latitude}` → next step).
|
|
18
25
|
- **Two HTTP connectors on two different hosts**, each with its own egress allowlist.
|
|
19
26
|
- **Query parameters** (`query: [...]`) and a constant query baked into the path (`?current_weather=true`).
|
|
20
|
-
- **Deep response mapping** with the `${...}` language
|
|
21
|
-
`${response.current_weather.temperature}`)
|
|
27
|
+
- **Deep response mapping** with the `${...}` language — single-element indexing
|
|
28
|
+
(`${response.results[0].latitude}`, `${response.current_weather.temperature}`) **and** whole-array
|
|
29
|
+
binding (`${response.results}` returns the entire list verbatim).
|
|
30
|
+
- **A list-returning connector + compute narrowing** — `geo.search_list` binds the whole `results`
|
|
31
|
+
array; `places.narrow` reduces each element to `{ id, label }` and normalizes the no-results case
|
|
32
|
+
to `[]`.
|
|
22
33
|
- **Sandboxed compute** (no network/fs/env/clock) turning raw numbers into conditions + advice.
|
|
23
34
|
- **Typed input/output schemas** emitted as JSON Schema 2020-12.
|
|
24
35
|
|
|
@@ -52,3 +63,23 @@ Example result (live data, abbreviated):
|
|
|
52
63
|
```
|
|
53
64
|
|
|
54
65
|
Try other cities (`Reykjavik`, `Singapore`, `Denver`) to see the conditions and advice change.
|
|
66
|
+
|
|
67
|
+
Call `search_places` to see the **list-returning** tool — one query, many matches:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
curl -s "$URL" \
|
|
71
|
+
-H 'content-type: application/json' \
|
|
72
|
+
-H 'accept: application/json, text/event-stream' \
|
|
73
|
+
-H 'mcp-protocol-version: 2025-11-25' \
|
|
74
|
+
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"search_places","arguments":{"query":"Springfield"}}}'
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```json
|
|
78
|
+
{
|
|
79
|
+
"places": [
|
|
80
|
+
{ "id": "4951788", "label": "Springfield, Massachusetts, United States" },
|
|
81
|
+
{ "id": "4250542", "label": "Springfield, Illinois, United States" },
|
|
82
|
+
{ "id": "4508722", "label": "Springfield, Ohio, United States" }
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
@@ -19,13 +19,13 @@ const geocoding = connector('open_meteo_geocoding')
|
|
|
19
19
|
method: 'GET',
|
|
20
20
|
path: '/v1/search',
|
|
21
21
|
query: ['name'],
|
|
22
|
-
input: { name:
|
|
23
|
-
output: {
|
|
24
|
-
latitude:
|
|
25
|
-
longitude:
|
|
26
|
-
place:
|
|
27
|
-
country:
|
|
28
|
-
},
|
|
22
|
+
input: z.object({ name: z.string() }),
|
|
23
|
+
output: z.object({
|
|
24
|
+
latitude: z.number(),
|
|
25
|
+
longitude: z.number(),
|
|
26
|
+
place: z.string().optional(),
|
|
27
|
+
country: z.string().optional(),
|
|
28
|
+
}),
|
|
29
29
|
response: {
|
|
30
30
|
latitude: '${response.results[0].latitude}',
|
|
31
31
|
longitude: '${response.results[0].longitude}',
|
|
@@ -33,6 +33,23 @@ const geocoding = connector('open_meteo_geocoding')
|
|
|
33
33
|
country: '${response.results[0].country}',
|
|
34
34
|
},
|
|
35
35
|
},
|
|
36
|
+
// A LIST-returning read. `${response.results}` binds the WHOLE array verbatim — a
|
|
37
|
+
// variable-length list of place objects — with no pagination (Open-Meteo returns every match in
|
|
38
|
+
// one page). Contrast the `search` op above, which indexes a single element (`results[0]`). To
|
|
39
|
+
// reduce each element to a few fields, narrow it in the `geo_places` compute connector below: a
|
|
40
|
+
// response mapping cannot iterate an array, and a tool's Zod output does not strip fields at
|
|
41
|
+
// runtime.
|
|
42
|
+
search_list: {
|
|
43
|
+
type: 'read',
|
|
44
|
+
method: 'GET',
|
|
45
|
+
path: '/v1/search',
|
|
46
|
+
query: ['name', 'count'],
|
|
47
|
+
input: z.object({ name: z.string(), count: z.number().optional() }),
|
|
48
|
+
output: z.object({ results: z.array(z.unknown()).optional() }),
|
|
49
|
+
response: {
|
|
50
|
+
results: '${response.results}',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
36
53
|
},
|
|
37
54
|
});
|
|
38
55
|
|
|
@@ -47,15 +64,12 @@ const forecast = connector('open_meteo_forecast')
|
|
|
47
64
|
method: 'GET',
|
|
48
65
|
path: '/v1/forecast?current_weather=true',
|
|
49
66
|
query: ['latitude', 'longitude'],
|
|
50
|
-
input: {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
windspeed: { type: 'number' },
|
|
57
|
-
weathercode: { type: 'number' },
|
|
58
|
-
},
|
|
67
|
+
input: z.object({ latitude: z.number(), longitude: z.number() }),
|
|
68
|
+
output: z.object({
|
|
69
|
+
temperature: z.number().optional(),
|
|
70
|
+
windspeed: z.number().optional(),
|
|
71
|
+
weathercode: z.number().optional(),
|
|
72
|
+
}),
|
|
59
73
|
response: {
|
|
60
74
|
temperature: '${response.current_weather.temperature}',
|
|
61
75
|
windspeed: '${response.current_weather.windspeed}',
|
|
@@ -69,18 +83,18 @@ const brief = connector('weather_brief')
|
|
|
69
83
|
.version('1.0.0')
|
|
70
84
|
.compute('summarize', {
|
|
71
85
|
type: 'read',
|
|
72
|
-
input: {
|
|
73
|
-
place:
|
|
74
|
-
country:
|
|
75
|
-
temperature:
|
|
76
|
-
windspeed:
|
|
77
|
-
weathercode:
|
|
78
|
-
},
|
|
79
|
-
output: {
|
|
80
|
-
conditions:
|
|
81
|
-
headline:
|
|
82
|
-
advice:
|
|
83
|
-
},
|
|
86
|
+
input: z.object({
|
|
87
|
+
place: z.string(),
|
|
88
|
+
country: z.string().optional(),
|
|
89
|
+
temperature: z.number(),
|
|
90
|
+
windspeed: z.number(),
|
|
91
|
+
weathercode: z.number(),
|
|
92
|
+
}),
|
|
93
|
+
output: z.object({
|
|
94
|
+
conditions: z.string(),
|
|
95
|
+
headline: z.string(),
|
|
96
|
+
advice: z.string(),
|
|
97
|
+
}),
|
|
84
98
|
// A real function — type-checked here, serialized to source and run in the sandbox. It must be
|
|
85
99
|
// self-contained: no imports, no closure over outer variables, synchronous.
|
|
86
100
|
run: (input) => {
|
|
@@ -132,12 +146,40 @@ const brief = connector('weather_brief')
|
|
|
132
146
|
},
|
|
133
147
|
});
|
|
134
148
|
|
|
149
|
+
// Narrowing a live list to `{ id, label }` summaries is the ONE reshape a response mapping cannot do
|
|
150
|
+
// (the `${...}` language has no per-item iteration) and a tool's Zod output does not enforce at runtime
|
|
151
|
+
// — so it happens here, in a sandboxed compute connector (a connector is HTTP or compute, not both).
|
|
152
|
+
// This also normalizes the no-results case (Open-Meteo omits `results` when nothing matches) to `[]`.
|
|
153
|
+
const placeNarrow = connector('geo_places')
|
|
154
|
+
.version('1.0.0')
|
|
155
|
+
.compute('narrow', {
|
|
156
|
+
type: 'read',
|
|
157
|
+
input: z.object({ results: z.unknown().optional() }),
|
|
158
|
+
output: z.object({ places: z.array(z.unknown()) }),
|
|
159
|
+
// Self-contained: no imports, no closure over outer variables, synchronous.
|
|
160
|
+
run: (input) => {
|
|
161
|
+
const raw = input.results;
|
|
162
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
163
|
+
const places = list.map((entry) => {
|
|
164
|
+
const parts = [entry.name, entry.admin1, entry.country].filter(
|
|
165
|
+
(part) => typeof part === 'string' && part.length > 0,
|
|
166
|
+
);
|
|
167
|
+
const id =
|
|
168
|
+
entry.id !== undefined && entry.id !== null
|
|
169
|
+
? String(entry.id)
|
|
170
|
+
: `${entry.latitude},${entry.longitude}`;
|
|
171
|
+
return { id, label: parts.join(', ') };
|
|
172
|
+
});
|
|
173
|
+
return { places };
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
135
177
|
export default server(
|
|
136
178
|
'weather_briefing',
|
|
137
179
|
{
|
|
138
180
|
title: 'Weather Briefing',
|
|
139
181
|
version: '1.0.0',
|
|
140
|
-
use: { geo: geocoding, forecast, brief },
|
|
182
|
+
use: { geo: geocoding, forecast, brief, places: placeNarrow },
|
|
141
183
|
branding: {
|
|
142
184
|
name: 'Weather Briefing',
|
|
143
185
|
accent: '#0284C7',
|
|
@@ -186,5 +228,22 @@ export default server(
|
|
|
186
228
|
};
|
|
187
229
|
},
|
|
188
230
|
}),
|
|
231
|
+
// A connector that returns a live, variable-length LIST: search a place name, get back the
|
|
232
|
+
// matching locations as `{ id, label }` options the model can resolve against. The HTTP op binds
|
|
233
|
+
// the whole array; the compute connector narrows each element to the two fields the model speaks
|
|
234
|
+
// from. Append new tools AFTER existing ones so `tools[0]` stays stable for host harnesses.
|
|
235
|
+
tool('search_places', {
|
|
236
|
+
description:
|
|
237
|
+
'Search a place name and return the matching locations as a list of { id, label } options.',
|
|
238
|
+
input: z.object({ query: z.string() }),
|
|
239
|
+
output: z.object({
|
|
240
|
+
places: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
241
|
+
}),
|
|
242
|
+
fulfil: ({ input, connectors }) => {
|
|
243
|
+
const found = connectors.geo.search_list({ name: input.query });
|
|
244
|
+
const narrowed = connectors.places.narrow({ results: found.results });
|
|
245
|
+
return { places: narrowed.places };
|
|
246
|
+
},
|
|
247
|
+
}),
|
|
189
248
|
],
|
|
190
249
|
);
|
|
@@ -5,4 +5,25 @@ describe('weather example', () => {
|
|
|
5
5
|
it('exports a Noodle server definition', () => {
|
|
6
6
|
expect(typeof app.toManifest).toBe('function');
|
|
7
7
|
});
|
|
8
|
+
|
|
9
|
+
it('adds a list-returning search_places tool without disturbing tool order', async () => {
|
|
10
|
+
const manifest = (await app.toManifest()) as {
|
|
11
|
+
tools: ReadonlyArray<{ name: string }>;
|
|
12
|
+
connectors?: Record<string, { id: string; version: string }>;
|
|
13
|
+
};
|
|
14
|
+
const toolNames = manifest.tools.map((t) => t.name);
|
|
15
|
+
// The original briefing tool stays first — host harnesses (the online e2e) read `tools[0]`,
|
|
16
|
+
// so new tools are appended, never prepended.
|
|
17
|
+
expect(toolNames[0]).toBe('weather_briefing');
|
|
18
|
+
expect(toolNames).toContain('search_places');
|
|
19
|
+
expect(toolNames.indexOf('search_places')).toBeGreaterThan(
|
|
20
|
+
toolNames.indexOf('weather_briefing'),
|
|
21
|
+
);
|
|
22
|
+
// The list is produced by a whole-array-bind HTTP op (`search_list`) narrowed to {id,label}
|
|
23
|
+
// through a separate compute connector (`geo_places`).
|
|
24
|
+
expect(manifest.connectors?.places).toEqual({ id: 'geo_places', version: '1.0.0' });
|
|
25
|
+
const wire = JSON.stringify(manifest);
|
|
26
|
+
expect(wire).toContain('search_list');
|
|
27
|
+
expect(wire).toContain('narrow');
|
|
28
|
+
});
|
|
8
29
|
});
|
|
@@ -55,16 +55,16 @@ const crm = connector('crm').version('1.0.0').http({
|
|
|
55
55
|
method: 'GET',
|
|
56
56
|
path: '/customers',
|
|
57
57
|
query: ['email'],
|
|
58
|
-
input: { email:
|
|
59
|
-
output: { id:
|
|
58
|
+
input: z.object({ email: z.string() }),
|
|
59
|
+
output: z.object({ id: z.string(), name: z.string().optional() }),
|
|
60
60
|
response: { id: '${response.data[0].id}', name: '${response.data[0].name}' },
|
|
61
61
|
},
|
|
62
62
|
create_ticket: {
|
|
63
63
|
type: 'action',
|
|
64
64
|
method: 'POST',
|
|
65
65
|
path: '/tickets',
|
|
66
|
-
input: { customer_id:
|
|
67
|
-
output: { ticket_id:
|
|
66
|
+
input: z.object({ customer_id: z.string(), body: z.string() }),
|
|
67
|
+
output: z.object({ ticket_id: z.string() }),
|
|
68
68
|
request: { customer_id: '${args.customer_id}', body: '${args.body}' },
|
|
69
69
|
response: { ticket_id: '${response.id}' },
|
|
70
70
|
},
|
|
@@ -120,16 +120,16 @@ const tasks = connector('tasks').version('1.0.0').http({
|
|
|
120
120
|
method: 'GET',
|
|
121
121
|
path: '/tasks',
|
|
122
122
|
query: ['query'],
|
|
123
|
-
input: { query:
|
|
124
|
-
output: { matches:
|
|
123
|
+
input: z.object({ query: z.string() }),
|
|
124
|
+
output: z.object({ matches: z.array(z.unknown()) }),
|
|
125
125
|
response: { matches: '${response.results}' },
|
|
126
126
|
},
|
|
127
127
|
close_task: {
|
|
128
128
|
type: 'action',
|
|
129
129
|
method: 'POST',
|
|
130
130
|
path: '/tasks/{id}/close',
|
|
131
|
-
input: { id:
|
|
132
|
-
output: { ok:
|
|
131
|
+
input: z.object({ id: z.string() }),
|
|
132
|
+
output: z.object({ ok: z.boolean() }),
|
|
133
133
|
response: { ok: '${response.ok}' },
|
|
134
134
|
},
|
|
135
135
|
},
|
|
@@ -163,8 +163,8 @@ The model never sees a task id from the user; `find_tasks` returns `{ id, title
|
|
|
163
163
|
|
|
164
164
|
```ts
|
|
165
165
|
const scoring = connector('scoring').version('1.0.0').compute('normalize', {
|
|
166
|
-
input: { email:
|
|
167
|
-
output: { score:
|
|
166
|
+
input: z.object({ email: z.string(), priority: z.string().optional() }),
|
|
167
|
+
output: z.object({ score: z.number() }),
|
|
168
168
|
calls: { find_customer: 'crm.find_customer' },
|
|
169
169
|
limits: { timeoutMs: 1000, maxHostCalls: 2 },
|
|
170
170
|
run(input, { callOperation }) {
|
|
@@ -8,7 +8,10 @@ drift. Probe the live API, learn the real shape, then encode it as a `connector`
|
|
|
8
8
|
- Secure the key first
|
|
9
9
|
- Probe the live API
|
|
10
10
|
- Model the connector from the observed shape
|
|
11
|
+
- Return a list
|
|
12
|
+
- Create, update, delete
|
|
11
13
|
- Design intent tools
|
|
14
|
+
- Set the secret for local runs
|
|
12
15
|
- Prove real output
|
|
13
16
|
- Then build the app
|
|
14
17
|
|
|
@@ -19,7 +22,7 @@ managed secret and reference it only as `secret(...)`:
|
|
|
19
22
|
|
|
20
23
|
```sh
|
|
21
24
|
export SOME_API_KEY=… # the user sets this; it never appears in a file or prompt
|
|
22
|
-
noodle secrets set SOME_API_KEY --from-env SOME_API_KEY
|
|
25
|
+
noodle secrets set SOME_API_KEY --runtime local --scope org --org local --from-env SOME_API_KEY # local-run scope — see "Set the secret for local runs"
|
|
23
26
|
```
|
|
24
27
|
|
|
25
28
|
In `server.ts` the key is only ever `secret("SOME_API_KEY")` — keep the raw value out of code, tests,
|
|
@@ -53,6 +56,51 @@ Encode the API as an HTTP connector, mapping only the fields you actually saw in
|
|
|
53
56
|
The full connector shape, every `auth.kind`, and compute connectors are in
|
|
54
57
|
`references/authoring-workflow.md`.
|
|
55
58
|
|
|
59
|
+
## Return a list
|
|
60
|
+
|
|
61
|
+
Most real tools return a variable-length list (search results, a user’s tasks). Bind the **whole array** — a single `${response.path}` returns the referenced value verbatim, arrays included:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// The API returns { results: [ { id, name, country, … }, … ] }
|
|
65
|
+
output: z.object({ places: z.array(z.unknown()) }),
|
|
66
|
+
response: { places: '${response.results}' },
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Three things that are easy to get subtly wrong:
|
|
70
|
+
|
|
71
|
+
- **A response mapping cannot iterate.** There is no per-item / `map` / `item` construct, so you cannot reshape `[{…30 fields}]` into `[{ id, label }]` inside a `response:` block — bind the whole array.
|
|
72
|
+
- **A tool's Zod `output` does not strip at runtime.** It only advertises the JSON Schema; the runtime returns your `fulfil` output verbatim, so `z.array(z.object({ id, label }))` will NOT drop extra element fields.
|
|
73
|
+
- **So narrow in a compute connector.** To reshape each element, synthesize a `label`, or normalize a missing array to `[]`, pass the whole array to a `.compute(...)` op whose `run` maps it (a connector is HTTP **or** compute, not both — use a second connector). To only *drop* known fields without reshaping, `projection: { hiddenFields: [...] }` deletes them from each element. Worked example: `examples/weather` — `search_list` binds the array, then `geo_places.narrow` reshapes to `{ id, label }` and normalizes no-results to `[]`; `examples/sharepoint-lists` shows the same pattern against a real API.
|
|
74
|
+
|
|
75
|
+
For a **paginated** API, collect across pages with a `pagination` config; the collected list is then `${response.items}`:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
pagination: {
|
|
79
|
+
kind: 'cursor', // or 'pageNumber'
|
|
80
|
+
items: '${response.results}', // the array on ONE page
|
|
81
|
+
nextCursor: '${response.next_cursor}', // 'pageNumber' uses hasMore + pageParam instead
|
|
82
|
+
cursorParam: 'cursor',
|
|
83
|
+
maxPages: 5, maxItems: 100,
|
|
84
|
+
},
|
|
85
|
+
// items / nextCursor / hasMore run over ONE raw page; your response mapping runs over the
|
|
86
|
+
// collected aggregate, so the full list is:
|
|
87
|
+
response: { tasks: '${response.items}' },
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Create, update, delete
|
|
91
|
+
|
|
92
|
+
Pair the read/list with the mutations your intent tools need:
|
|
93
|
+
- **Create / update** — `method: 'POST'` / `'PATCH'`; the request body is authored as `request: { field: '${input.x}' }` — the `request` object **is** the JSON body (do not nest it under `body`), and URL query params are the operation-level `query: [...]` array.
|
|
94
|
+
- **Delete / close** — many endpoints return `204 No Content`. Set `responseType: 'empty'`, which enforces the status and binds `{}` (there is no body to map).
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
close_task: {
|
|
98
|
+
type: 'action', method: 'POST', path: '/tasks/{id}/close',
|
|
99
|
+
input: z.object({ id: z.string() }),
|
|
100
|
+
responseType: 'empty',
|
|
101
|
+
},
|
|
102
|
+
```
|
|
103
|
+
|
|
56
104
|
## Design intent tools
|
|
57
105
|
|
|
58
106
|
Shape tools around what the user says, not 1:1 around endpoints. Pair an id-taking action with a
|
|
@@ -60,6 +108,20 @@ find/search operation that returns `{ id, label }` summaries so the model resolv
|
|
|
60
108
|
and map each response to a few labelled fields the model can speak from. See the "Design tools for the
|
|
61
109
|
model" section of `references/authoring-workflow.md`.
|
|
62
110
|
|
|
111
|
+
## Set the secret for local runs
|
|
112
|
+
|
|
113
|
+
`noodle secrets set NAME --from-env NAME` **without a scope** writes to your global target (or errors) — which a local `noodle dev` never reads. Local `dev` resolves secrets under `org=local`, `app=<project-dir-slug>`, `env=dev` (the `…/o/local/<app>/dev/mcp` URL it prints). Set the secret at a matching local scope:
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
# Simplest — org scope is visible to every local app. Pin --runtime local so a cloud login
|
|
117
|
+
# (a non-local default runtime) does not send it to the hosted control plane:
|
|
118
|
+
noodle secrets set SOME_API_KEY --runtime local --scope org --org local --from-env SOME_API_KEY
|
|
119
|
+
# Or the exact env scope, using the app slug from the printed dev URL:
|
|
120
|
+
noodle secrets set SOME_API_KEY --runtime local --scope env --org local --app <app-slug> --env dev --from-env SOME_API_KEY
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Local secrets live in `./.env.noodle` (never commit it). **Symptom to recognize:** a required `secret(...)` that can’t resolve fails compile *closed*, so nothing is served. `noodle tools call` / `noodle test` / `noodle dev` name this directly as `connector_secret_unresolved` with the exact scoped-secret fix; an external MCP client (Inspector/mcpjam) hitting the loopback still sees an opaque `-32600 "not found"`. Either way, fix the secret’s scope, not the connector.
|
|
124
|
+
|
|
63
125
|
## Prove real output
|
|
64
126
|
|
|
65
127
|
`noodle validate` / `noodle test` prove a connector tool *compiles and registers* — not that its
|