@noodleseed/agent-kit 0.14.0 → 0.16.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 +549 -9
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +6 -4
- package/skills/claude-code/examples/acme-bistro/README.md +51 -0
- package/skills/claude-code/examples/acme-bistro/design/UX-Document.md +435 -0
- package/skills/claude-code/examples/acme-bistro/design/api-contract.md +161 -0
- package/skills/claude-code/examples/acme-bistro/design/wireframe.html +573 -0
- package/skills/claude-code/examples/acme-bistro/noodle.json +5 -0
- package/skills/claude-code/examples/acme-bistro/package.json +20 -0
- package/skills/claude-code/examples/acme-bistro/src/helpers.ts +7 -0
- package/skills/claude-code/examples/acme-bistro/src/server.ts +149 -0
- package/skills/claude-code/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
- package/skills/claude-code/examples/acme-bistro/src/views/widget-style.css +228 -0
- package/skills/claude-code/examples/acme-bistro/test/server.test.ts +21 -0
- package/skills/claude-code/examples/acme-bistro/vitest.config.ts +7 -0
- package/skills/claude-code/examples/acme-discovery/README.md +85 -0
- package/skills/claude-code/examples/acme-discovery/design/UX-Document.md +441 -0
- package/skills/claude-code/examples/acme-discovery/design/wireframe.html +670 -0
- package/skills/claude-code/examples/acme-discovery/noodle.json +5 -0
- package/skills/claude-code/examples/acme-discovery/package.json +20 -0
- package/skills/claude-code/examples/acme-discovery/src/helpers.ts +7 -0
- package/skills/claude-code/examples/acme-discovery/src/server.ts +202 -0
- package/skills/claude-code/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
- package/skills/claude-code/examples/acme-discovery/src/views/widget-style.css +207 -0
- package/skills/claude-code/examples/acme-discovery/test/server.test.ts +24 -0
- package/skills/claude-code/examples/acme-discovery/vitest.config.ts +7 -0
- package/skills/claude-code/examples/acme-tasks/README.md +80 -0
- package/skills/claude-code/examples/acme-tasks/design/UX-Document.md +277 -0
- package/skills/claude-code/examples/acme-tasks/design/wireframe.html +603 -0
- package/skills/claude-code/examples/acme-tasks/noodle.json +5 -0
- package/skills/claude-code/examples/acme-tasks/package.json +20 -0
- package/skills/claude-code/examples/acme-tasks/src/helpers.ts +6 -0
- package/skills/claude-code/examples/acme-tasks/src/server.ts +141 -0
- package/skills/claude-code/examples/acme-tasks/src/views/task-list.tsx +177 -0
- package/skills/claude-code/examples/acme-tasks/src/views/widget-style.css +227 -0
- package/skills/claude-code/examples/acme-tasks/test/server.test.ts +22 -0
- package/skills/claude-code/examples/acme-tasks/vitest.config.ts +7 -0
- package/skills/claude-code/examples/food-ordering/README.md +82 -0
- package/skills/claude-code/examples/food-ordering/noodle.json +5 -0
- package/skills/claude-code/examples/food-ordering/package.json +22 -0
- package/skills/claude-code/examples/food-ordering/src/helpers.ts +34 -0
- package/skills/claude-code/examples/food-ordering/src/server.ts +412 -0
- package/skills/claude-code/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
- package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
- package/skills/claude-code/examples/food-ordering/src/views/widget-style.css +682 -0
- package/skills/claude-code/examples/food-ordering/test/server.test.ts +58 -0
- package/skills/claude-code/examples/food-ordering/vitest.config.ts +30 -0
- package/skills/claude-code/examples/hello/README.md +13 -0
- package/skills/claude-code/examples/hello/noodle.json +5 -0
- package/skills/claude-code/examples/hello/package.json +16 -0
- package/skills/claude-code/examples/hello/src/server.ts +29 -0
- package/skills/claude-code/examples/hello/test/server.test.ts +8 -0
- package/skills/claude-code/examples/weather/README.md +85 -0
- package/skills/claude-code/examples/weather/noodle.json +4 -0
- package/skills/claude-code/examples/weather/package.json +16 -0
- package/skills/claude-code/examples/weather/src/server.ts +261 -0
- package/skills/claude-code/examples/weather/test/server.test.ts +29 -0
- package/skills/claude-code/references/connect-an-api.md +63 -1
- package/skills/claude-code/references/examples.md +16 -7
- package/skills/claude-code/references/troubleshooting.md +2 -0
- package/skills/codex/SKILL.md +6 -4
- package/skills/codex/examples/acme-bistro/README.md +51 -0
- package/skills/codex/examples/acme-bistro/design/UX-Document.md +435 -0
- package/skills/codex/examples/acme-bistro/design/api-contract.md +161 -0
- package/skills/codex/examples/acme-bistro/design/wireframe.html +573 -0
- package/skills/codex/examples/acme-bistro/noodle.json +5 -0
- package/skills/codex/examples/acme-bistro/package.json +20 -0
- package/skills/codex/examples/acme-bistro/src/helpers.ts +7 -0
- package/skills/codex/examples/acme-bistro/src/server.ts +149 -0
- package/skills/codex/examples/acme-bistro/src/views/menu-cart.tsx +159 -0
- package/skills/codex/examples/acme-bistro/src/views/widget-style.css +228 -0
- package/skills/codex/examples/acme-bistro/test/server.test.ts +21 -0
- package/skills/codex/examples/acme-bistro/vitest.config.ts +7 -0
- package/skills/codex/examples/acme-discovery/README.md +85 -0
- package/skills/codex/examples/acme-discovery/design/UX-Document.md +441 -0
- package/skills/codex/examples/acme-discovery/design/wireframe.html +670 -0
- package/skills/codex/examples/acme-discovery/noodle.json +5 -0
- package/skills/codex/examples/acme-discovery/package.json +20 -0
- package/skills/codex/examples/acme-discovery/src/helpers.ts +7 -0
- package/skills/codex/examples/acme-discovery/src/server.ts +202 -0
- package/skills/codex/examples/acme-discovery/src/views/discovery-carousel.tsx +155 -0
- package/skills/codex/examples/acme-discovery/src/views/widget-style.css +207 -0
- package/skills/codex/examples/acme-discovery/test/server.test.ts +24 -0
- package/skills/codex/examples/acme-discovery/vitest.config.ts +7 -0
- package/skills/codex/examples/acme-tasks/README.md +80 -0
- package/skills/codex/examples/acme-tasks/design/UX-Document.md +277 -0
- package/skills/codex/examples/acme-tasks/design/wireframe.html +603 -0
- package/skills/codex/examples/acme-tasks/noodle.json +5 -0
- package/skills/codex/examples/acme-tasks/package.json +20 -0
- package/skills/codex/examples/acme-tasks/src/helpers.ts +6 -0
- package/skills/codex/examples/acme-tasks/src/server.ts +141 -0
- package/skills/codex/examples/acme-tasks/src/views/task-list.tsx +177 -0
- package/skills/codex/examples/acme-tasks/src/views/widget-style.css +227 -0
- package/skills/codex/examples/acme-tasks/test/server.test.ts +22 -0
- package/skills/codex/examples/acme-tasks/vitest.config.ts +7 -0
- package/skills/codex/examples/food-ordering/README.md +82 -0
- package/skills/codex/examples/food-ordering/noodle.json +5 -0
- package/skills/codex/examples/food-ordering/package.json +22 -0
- package/skills/codex/examples/food-ordering/src/helpers.ts +34 -0
- package/skills/codex/examples/food-ordering/src/server.ts +412 -0
- package/skills/codex/examples/food-ordering/src/views/capabilities-card.tsx +155 -0
- package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +659 -0
- package/skills/codex/examples/food-ordering/src/views/widget-style.css +682 -0
- package/skills/codex/examples/food-ordering/test/server.test.ts +58 -0
- package/skills/codex/examples/food-ordering/vitest.config.ts +30 -0
- package/skills/codex/examples/hello/README.md +13 -0
- package/skills/codex/examples/hello/noodle.json +5 -0
- package/skills/codex/examples/hello/package.json +16 -0
- package/skills/codex/examples/hello/src/server.ts +29 -0
- package/skills/codex/examples/hello/test/server.test.ts +8 -0
- package/skills/codex/examples/weather/README.md +85 -0
- package/skills/codex/examples/weather/noodle.json +4 -0
- package/skills/codex/examples/weather/package.json +16 -0
- package/skills/codex/examples/weather/src/server.ts +261 -0
- package/skills/codex/examples/weather/test/server.test.ts +29 -0
- package/skills/codex/references/connect-an-api.md +63 -1
- package/skills/codex/references/examples.md +16 -7
- package/skills/codex/references/troubleshooting.md +2 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
resolve: {
|
|
5
|
+
alias: {
|
|
6
|
+
'@noodleseed/one': new URL('../../packages/authoring/src/index.ts', import.meta.url).pathname,
|
|
7
|
+
'@noodle-borg/capabilities': new URL(
|
|
8
|
+
'../../packages/capabilities/src/index.ts',
|
|
9
|
+
import.meta.url,
|
|
10
|
+
).pathname,
|
|
11
|
+
'@noodle-borg/compiler': new URL('../../packages/compiler/src/index.ts', import.meta.url)
|
|
12
|
+
.pathname,
|
|
13
|
+
'@noodle-borg/compute': new URL('../../packages/compute/src/index.ts', import.meta.url)
|
|
14
|
+
.pathname,
|
|
15
|
+
'@noodle-borg/connector-defs': new URL(
|
|
16
|
+
'../../packages/connector-defs/src/index.ts',
|
|
17
|
+
import.meta.url,
|
|
18
|
+
).pathname,
|
|
19
|
+
'@noodle-borg/connector-http': new URL(
|
|
20
|
+
'../../packages/connector-http/src/index.ts',
|
|
21
|
+
import.meta.url,
|
|
22
|
+
).pathname,
|
|
23
|
+
'@noodle-borg/runtime': new URL('../../packages/runtime/src/index.ts', import.meta.url)
|
|
24
|
+
.pathname,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
test: {
|
|
28
|
+
include: ['test/**/*.test.ts'],
|
|
29
|
+
},
|
|
30
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# hello — minimal TypeScript quickstart
|
|
2
|
+
|
|
3
|
+
The smallest deployable Noodle app: a single `greet` tool authored in TypeScript with no
|
|
4
|
+
connectors, secrets, flows, widgets, or handoff policy. It still uses the current server options form
|
|
5
|
+
so new authors see where server-level branding belongs. Use it to smoke the author loop
|
|
6
|
+
(`noodle validate` / `noodle dev`) or a first deploy.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
noodle dev examples/hello/src/server.ts --app hello
|
|
10
|
+
noodle deploy examples/hello/src/server.ts --org acme --app hello
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
It is also the fixture for `pnpm smoke:dev` and the e2e harness, so keep its tool surface stable.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hello",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"validate": "noodle validate",
|
|
9
|
+
"dev": "noodle dev",
|
|
10
|
+
"deploy": "noodle deploy"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@noodleseed/one": "latest",
|
|
14
|
+
"vitest": "latest"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { server, tool, z } from '@noodleseed/one';
|
|
2
|
+
|
|
3
|
+
export default server(
|
|
4
|
+
'hello',
|
|
5
|
+
{
|
|
6
|
+
title: 'Hello',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
branding: {
|
|
9
|
+
name: 'Hello',
|
|
10
|
+
accent: '#1D9E75',
|
|
11
|
+
radius: 'md',
|
|
12
|
+
density: 'comfortable',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
[
|
|
16
|
+
tool('greet', {
|
|
17
|
+
description: 'Greet someone by name.',
|
|
18
|
+
input: z.object({
|
|
19
|
+
name: z.string(),
|
|
20
|
+
}),
|
|
21
|
+
output: z.object({
|
|
22
|
+
message: z.string(),
|
|
23
|
+
}),
|
|
24
|
+
fulfil: ({ input }) => {
|
|
25
|
+
return { message: `Hello, ${input.name}!` };
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Weather Briefing
|
|
2
|
+
|
|
3
|
+
Two declarative tools that show the runtime's breadth working together, with **no auth and no API
|
|
4
|
+
keys**. The `weather_briefing` tool takes a city name and runs a **three-step flow**:
|
|
5
|
+
|
|
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.
|
|
9
|
+
|
|
10
|
+
1. **`geo.search`** → geocode the city to coordinates (Open-Meteo Geocoding API)
|
|
11
|
+
2. **`forecast.current`** → fetch current weather for those coordinates (Open-Meteo Forecast API)
|
|
12
|
+
3. **`brief.summarize`** → derive a human-readable briefing in a **WASM/QuickJS compute sandbox**
|
|
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
|
+
|
|
20
|
+
It exercises, in one TypeScript-authored app:
|
|
21
|
+
|
|
22
|
+
- **Server-level branding** with semantic tokens carried through the runtime artifact for any generated
|
|
23
|
+
app surface.
|
|
24
|
+
- **Ordered flow execution** with outputs threaded between steps (`${steps.geo.latitude}` → next step).
|
|
25
|
+
- **Two HTTP connectors on two different hosts**, each with its own egress allowlist.
|
|
26
|
+
- **Query parameters** (`query: [...]`) and a constant query baked into the path (`?current_weather=true`).
|
|
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 `[]`.
|
|
33
|
+
- **Sandboxed compute** (no network/fs/env/clock) turning raw numbers into conditions + advice.
|
|
34
|
+
- **Typed input/output schemas** emitted as JSON Schema 2020-12.
|
|
35
|
+
|
|
36
|
+
## Run it locally
|
|
37
|
+
|
|
38
|
+
From the repo root, with the workspace built (`pnpm build`):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
: # 1. boot the local loopback dev server
|
|
42
|
+
node packages/cli/dist/cli.js dev examples/weather/src/server.ts --app weather
|
|
43
|
+
|
|
44
|
+
: # 2. in another shell, call the printed local endpoint
|
|
45
|
+
URL=http://127.0.0.1:<port>/o/local/weather/dev/mcp
|
|
46
|
+
curl -s "$URL" \
|
|
47
|
+
-H 'content-type: application/json' \
|
|
48
|
+
-H 'accept: application/json, text/event-stream' \
|
|
49
|
+
-H 'mcp-protocol-version: 2025-11-25' \
|
|
50
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"weather_briefing","arguments":{"city":"Paris"}}}'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Example result (live data, abbreviated):
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"place": "Paris", "country": "France",
|
|
58
|
+
"temperature_c": 25.1, "windspeed_kmh": 8.3,
|
|
59
|
+
"conditions": "overcast",
|
|
60
|
+
"headline": "Paris, France: 25°C, overcast.",
|
|
61
|
+
"advice": "Comfortable conditions — no special prep needed."
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
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
|
+
```
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "weather",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"validate": "noodle validate",
|
|
9
|
+
"dev": "noodle dev",
|
|
10
|
+
"deploy": "noodle deploy"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@noodleseed/one": "latest",
|
|
14
|
+
"vitest": "latest"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { connector, server, tool, z } from '@noodleseed/one';
|
|
2
|
+
|
|
3
|
+
// The same Weather Briefing server, authored in TypeScript with the Noodle authoring SDK.
|
|
4
|
+
//
|
|
5
|
+
// The SDK owns the *manifest*: the tool, its Zod-typed input/output schemas, and the flow — which you
|
|
6
|
+
// write as ordinary code in `fulfil` and the SDK records symbolically into ordered steps.
|
|
7
|
+
//
|
|
8
|
+
// HTTP and compute connectors are authored here too, so the public developer entrypoint is one
|
|
9
|
+
// self-contained server.ts. The SDK still compiles this to internal manifest/catalog data for the runtime.
|
|
10
|
+
|
|
11
|
+
const geocoding = connector('open_meteo_geocoding')
|
|
12
|
+
.version('1.0.0')
|
|
13
|
+
.http({
|
|
14
|
+
baseUrl: 'https://geocoding-api.open-meteo.com',
|
|
15
|
+
allowedOrigins: ['https://geocoding-api.open-meteo.com'],
|
|
16
|
+
operations: {
|
|
17
|
+
search: {
|
|
18
|
+
type: 'read',
|
|
19
|
+
method: 'GET',
|
|
20
|
+
path: '/v1/search',
|
|
21
|
+
query: ['name'],
|
|
22
|
+
input: { name: { type: 'string', required: true } },
|
|
23
|
+
output: {
|
|
24
|
+
latitude: { type: 'number', required: true },
|
|
25
|
+
longitude: { type: 'number', required: true },
|
|
26
|
+
place: { type: 'string' },
|
|
27
|
+
country: { type: 'string' },
|
|
28
|
+
},
|
|
29
|
+
response: {
|
|
30
|
+
latitude: '${response.results[0].latitude}',
|
|
31
|
+
longitude: '${response.results[0].longitude}',
|
|
32
|
+
place: '${response.results[0].name}',
|
|
33
|
+
country: '${response.results[0].country}',
|
|
34
|
+
},
|
|
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: {
|
|
48
|
+
name: { type: 'string', required: true },
|
|
49
|
+
count: { type: 'number' },
|
|
50
|
+
},
|
|
51
|
+
output: {
|
|
52
|
+
results: { type: 'array' },
|
|
53
|
+
},
|
|
54
|
+
response: {
|
|
55
|
+
results: '${response.results}',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const forecast = connector('open_meteo_forecast')
|
|
62
|
+
.version('1.0.0')
|
|
63
|
+
.http({
|
|
64
|
+
baseUrl: 'https://api.open-meteo.com',
|
|
65
|
+
allowedOrigins: ['https://api.open-meteo.com'],
|
|
66
|
+
operations: {
|
|
67
|
+
current: {
|
|
68
|
+
type: 'read',
|
|
69
|
+
method: 'GET',
|
|
70
|
+
path: '/v1/forecast?current_weather=true',
|
|
71
|
+
query: ['latitude', 'longitude'],
|
|
72
|
+
input: {
|
|
73
|
+
latitude: { type: 'number', required: true },
|
|
74
|
+
longitude: { type: 'number', required: true },
|
|
75
|
+
},
|
|
76
|
+
output: {
|
|
77
|
+
temperature: { type: 'number' },
|
|
78
|
+
windspeed: { type: 'number' },
|
|
79
|
+
weathercode: { type: 'number' },
|
|
80
|
+
},
|
|
81
|
+
response: {
|
|
82
|
+
temperature: '${response.current_weather.temperature}',
|
|
83
|
+
windspeed: '${response.current_weather.windspeed}',
|
|
84
|
+
weathercode: '${response.current_weather.weathercode}',
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const brief = connector('weather_brief')
|
|
91
|
+
.version('1.0.0')
|
|
92
|
+
.compute('summarize', {
|
|
93
|
+
type: 'read',
|
|
94
|
+
input: {
|
|
95
|
+
place: { type: 'string', required: true },
|
|
96
|
+
country: { type: 'string' },
|
|
97
|
+
temperature: { type: 'number', required: true },
|
|
98
|
+
windspeed: { type: 'number', required: true },
|
|
99
|
+
weathercode: { type: 'number', required: true },
|
|
100
|
+
},
|
|
101
|
+
output: {
|
|
102
|
+
conditions: { type: 'string', required: true },
|
|
103
|
+
headline: { type: 'string', required: true },
|
|
104
|
+
advice: { type: 'string', required: true },
|
|
105
|
+
},
|
|
106
|
+
// A real function — type-checked here, serialized to source and run in the sandbox. It must be
|
|
107
|
+
// self-contained: no imports, no closure over outer variables, synchronous.
|
|
108
|
+
run: (input) => {
|
|
109
|
+
const codes: Record<number, string> = {
|
|
110
|
+
0: 'clear sky',
|
|
111
|
+
1: 'mainly clear',
|
|
112
|
+
2: 'partly cloudy',
|
|
113
|
+
3: 'overcast',
|
|
114
|
+
45: 'fog',
|
|
115
|
+
48: 'depositing rime fog',
|
|
116
|
+
51: 'light drizzle',
|
|
117
|
+
53: 'moderate drizzle',
|
|
118
|
+
55: 'dense drizzle',
|
|
119
|
+
61: 'slight rain',
|
|
120
|
+
63: 'moderate rain',
|
|
121
|
+
65: 'heavy rain',
|
|
122
|
+
71: 'slight snow',
|
|
123
|
+
73: 'moderate snow',
|
|
124
|
+
75: 'heavy snow',
|
|
125
|
+
77: 'snow grains',
|
|
126
|
+
80: 'slight rain showers',
|
|
127
|
+
81: 'moderate rain showers',
|
|
128
|
+
82: 'violent rain showers',
|
|
129
|
+
85: 'slight snow showers',
|
|
130
|
+
86: 'heavy snow showers',
|
|
131
|
+
95: 'thunderstorm',
|
|
132
|
+
96: 'thunderstorm with hail',
|
|
133
|
+
99: 'thunderstorm with heavy hail',
|
|
134
|
+
};
|
|
135
|
+
const code = Number(input.weathercode);
|
|
136
|
+
const conditions = codes[code] || 'unknown conditions';
|
|
137
|
+
const temp = Math.round(Number(input.temperature));
|
|
138
|
+
const wind = Math.round(Number(input.windspeed));
|
|
139
|
+
const where = input.country ? `${input.place}, ${input.country}` : input.place;
|
|
140
|
+
const headline = `${where}: ${temp}°C, ${conditions}.`;
|
|
141
|
+
const tips: string[] = [];
|
|
142
|
+
if (temp <= 0) tips.push("bundle up, it's freezing");
|
|
143
|
+
else if (temp <= 10) tips.push('wear a warm coat');
|
|
144
|
+
else if (temp >= 28) tips.push("stay hydrated, it's hot");
|
|
145
|
+
if (code >= 95) tips.push('thunderstorms expected — seek shelter');
|
|
146
|
+
else if (code >= 71 && code <= 86 && code !== 80 && code !== 81 && code !== 82)
|
|
147
|
+
tips.push('snow — dress warm and tread carefully');
|
|
148
|
+
else if (code >= 51 && code <= 82) tips.push('bring an umbrella');
|
|
149
|
+
if (wind >= 30) tips.push('expect strong winds');
|
|
150
|
+
const advice = tips.length
|
|
151
|
+
? `${tips.join('; ')}.`
|
|
152
|
+
: 'Comfortable conditions — no special prep needed.';
|
|
153
|
+
return { conditions, headline, advice };
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Narrowing a live list to `{ id, label }` summaries is the ONE reshape a response mapping cannot do
|
|
158
|
+
// (the `${...}` language has no per-item iteration) and a tool's Zod output does not enforce at runtime
|
|
159
|
+
// — so it happens here, in a sandboxed compute connector (a connector is HTTP or compute, not both).
|
|
160
|
+
// This also normalizes the no-results case (Open-Meteo omits `results` when nothing matches) to `[]`.
|
|
161
|
+
const placeNarrow = connector('geo_places')
|
|
162
|
+
.version('1.0.0')
|
|
163
|
+
.compute('narrow', {
|
|
164
|
+
type: 'read',
|
|
165
|
+
input: {
|
|
166
|
+
results: { type: 'unknown' },
|
|
167
|
+
},
|
|
168
|
+
output: {
|
|
169
|
+
places: { type: 'array', required: true },
|
|
170
|
+
},
|
|
171
|
+
// Self-contained: no imports, no closure over outer variables, synchronous.
|
|
172
|
+
run: (input) => {
|
|
173
|
+
const raw = input.results;
|
|
174
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
175
|
+
const places = list.map((entry) => {
|
|
176
|
+
const parts = [entry.name, entry.admin1, entry.country].filter(
|
|
177
|
+
(part) => typeof part === 'string' && part.length > 0,
|
|
178
|
+
);
|
|
179
|
+
const id =
|
|
180
|
+
entry.id !== undefined && entry.id !== null
|
|
181
|
+
? String(entry.id)
|
|
182
|
+
: `${entry.latitude},${entry.longitude}`;
|
|
183
|
+
return { id, label: parts.join(', ') };
|
|
184
|
+
});
|
|
185
|
+
return { places };
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
export default server(
|
|
190
|
+
'weather_briefing',
|
|
191
|
+
{
|
|
192
|
+
title: 'Weather Briefing',
|
|
193
|
+
version: '1.0.0',
|
|
194
|
+
use: { geo: geocoding, forecast, brief, places: placeNarrow },
|
|
195
|
+
branding: {
|
|
196
|
+
name: 'Weather Briefing',
|
|
197
|
+
accent: '#0284C7',
|
|
198
|
+
radius: 'md',
|
|
199
|
+
density: 'comfortable',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
[
|
|
203
|
+
tool('weather_briefing', {
|
|
204
|
+
description:
|
|
205
|
+
'Look up a city, fetch its current weather, and return a human-readable briefing. Runs a ' +
|
|
206
|
+
'three-step flow: geocode the city, fetch the forecast, then derive the briefing in a sandboxed compute step.',
|
|
207
|
+
input: z.object({
|
|
208
|
+
city: z.string(),
|
|
209
|
+
}),
|
|
210
|
+
output: z.object({
|
|
211
|
+
place: z.string(),
|
|
212
|
+
country: z.string(),
|
|
213
|
+
temperature_c: z.number(),
|
|
214
|
+
windspeed_kmh: z.number(),
|
|
215
|
+
conditions: z.string(),
|
|
216
|
+
headline: z.string(),
|
|
217
|
+
advice: z.string(),
|
|
218
|
+
}),
|
|
219
|
+
fulfil: ({ input, connectors }) => {
|
|
220
|
+
const located = connectors.geo.search({ name: input.city });
|
|
221
|
+
const weather = connectors.forecast.current({
|
|
222
|
+
latitude: located.latitude,
|
|
223
|
+
longitude: located.longitude,
|
|
224
|
+
});
|
|
225
|
+
const briefing = connectors.brief.summarize({
|
|
226
|
+
place: located.place,
|
|
227
|
+
country: located.country,
|
|
228
|
+
temperature: weather.temperature,
|
|
229
|
+
windspeed: weather.windspeed,
|
|
230
|
+
weathercode: weather.weathercode,
|
|
231
|
+
});
|
|
232
|
+
return {
|
|
233
|
+
place: located.place,
|
|
234
|
+
country: located.country,
|
|
235
|
+
temperature_c: weather.temperature,
|
|
236
|
+
windspeed_kmh: weather.windspeed,
|
|
237
|
+
conditions: briefing.conditions,
|
|
238
|
+
headline: briefing.headline,
|
|
239
|
+
advice: briefing.advice,
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
}),
|
|
243
|
+
// A connector that returns a live, variable-length LIST: search a place name, get back the
|
|
244
|
+
// matching locations as `{ id, label }` options the model can resolve against. The HTTP op binds
|
|
245
|
+
// the whole array; the compute connector narrows each element to the two fields the model speaks
|
|
246
|
+
// from. Append new tools AFTER existing ones so `tools[0]` stays stable for host harnesses.
|
|
247
|
+
tool('search_places', {
|
|
248
|
+
description:
|
|
249
|
+
'Search a place name and return the matching locations as a list of { id, label } options.',
|
|
250
|
+
input: z.object({ query: z.string() }),
|
|
251
|
+
output: z.object({
|
|
252
|
+
places: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
253
|
+
}),
|
|
254
|
+
fulfil: ({ input, connectors }) => {
|
|
255
|
+
const found = connectors.geo.search_list({ name: input.query });
|
|
256
|
+
const narrowed = connectors.places.narrow({ results: found.results });
|
|
257
|
+
return { places: narrowed.places };
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
],
|
|
261
|
+
);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import app from '../src/server.js';
|
|
3
|
+
|
|
4
|
+
describe('weather example', () => {
|
|
5
|
+
it('exports a Noodle server definition', () => {
|
|
6
|
+
expect(typeof app.toManifest).toBe('function');
|
|
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
|
+
});
|
|
29
|
+
});
|
|
@@ -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: { places: { type: 'array' } },
|
|
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: { id: { type: 'string', required: true } },
|
|
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
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
# Examples
|
|
2
2
|
|
|
3
|
-
Flagship examples
|
|
3
|
+
Flagship examples, one per capability. The **bundled** rows ship inside this skill under `examples/<name>/` (the real `server.ts`, `src/views/*.tsx`, and — for the design-first flagships — a `design/` set); read them locally at the paths shown. The rest live only in the Noodle Seed repository under `examples/<name>/` on GitHub. Extend an existing flagship rather than inventing a new shape.
|
|
4
|
+
|
|
5
|
+
## Bundled in this skill — read locally
|
|
6
|
+
|
|
7
|
+
Paths are relative to this skill directory. Assets (images/fonts) are omitted from the bundle; fetch the full runnable example from the repository if you need them. Verify any edit with `noodle validate --json` then `noodle test --json`.
|
|
8
|
+
|
|
9
|
+
| Example | Use when | Read |
|
|
10
|
+
| :-- | :-- | :-- |
|
|
11
|
+
| `hello` | Minimal TypeScript quickstart — a single tool, no connectors/widgets. | `examples/hello/src/server.ts` |
|
|
12
|
+
| `weather` | HTTP connectors, multi-step flows, a list-returning connector op (whole-array bind + compute narrowing), and the sandboxed compute connector. | `examples/weather/src/server.ts` |
|
|
13
|
+
| `food-ordering` | Consumer ordering MCP App widgets, app-only helpers, cart state, assets, branding, and handoff. | `examples/food-ordering/src/server.ts` |
|
|
14
|
+
| `acme-discovery` | Top-of-funnel discovery→handoff: a discovery carousel, a `create_handoff` deep link, and a design-first UX spec + wireframe. | `examples/acme-discovery/src/server.ts` + `design/` |
|
|
15
|
+
| `acme-tasks` | A two-way productivity app designed around its top-3 prioritized flows (capture/prioritize/complete), with a design-first flow spec + wireframe. | `examples/acme-tasks/src/server.ts` + `design/` |
|
|
16
|
+
| `acme-bistro` | End-to-end ordering with a payment-only handoff; ships a gold-standard `design/` set (UX doc, wireframe with compliance audit, API contract). | `examples/acme-bistro/src/server.ts` + `design/` |
|
|
17
|
+
|
|
18
|
+
## In the repository only — `examples/<name>/` on GitHub
|
|
4
19
|
|
|
5
20
|
| Example | Use when |
|
|
6
21
|
| :-- | :-- |
|
|
7
|
-
| `hello` | Minimal TypeScript quickstart — a single tool, no connectors/widgets. |
|
|
8
|
-
| `weather` | HTTP connectors, multi-step flows, and the sandboxed compute connector. |
|
|
9
|
-
| `food-ordering` | Consumer ordering MCP App widgets, app-only helpers, cart state, assets, branding, and handoff. |
|
|
10
|
-
| `acme-discovery` | Top-of-funnel discovery→handoff: a discovery carousel, a `create_handoff` deep link, and a design-first UX spec + wireframe. |
|
|
11
|
-
| `acme-tasks` | A two-way productivity app designed around its top-3 prioritized flows (capture/prioritize/complete), with a design-first flow spec + wireframe. |
|
|
12
|
-
| `acme-bistro` | End-to-end ordering with a payment-only handoff; ships a gold-standard `design/` set (UX doc, wireframe with compliance audit, API contract). |
|
|
13
22
|
| `customer-auth` | End-user (customer) auth via OIDC/Firebase bridge with delegated credentials. |
|
|
14
23
|
| `stateful-draft` | Durable, caller-scoped widget state handles with optimistic revisions. |
|
|
15
24
|
| `perplexity` | A real SaaS API with bearer auth and a managed `secret`. |
|
|
@@ -26,5 +26,7 @@ For protocol/conformance checks, the headless harness is `@mcpjam/cli`, not a `n
|
|
|
26
26
|
| Hosted endpoint returns 401 to probes | Expected: hosted servers challenge unauthenticated calls with OAuth metadata | Sign in from the host when prompted; widen who may call with `noodle access set` if testers are outside the org |
|
|
27
27
|
| Tools error only after deploy | Runtime/config differences surface hosted (secrets, connector reachability) | Run `noodle smoke`, then `noodle metrics --agent-output` and `noodle events --tool <name> --status tool_error --json`; check `noodle secrets list` scope |
|
|
28
28
|
| A connector tool validates and lists, but returns empty or `undefined` fields | The `response` mapping references a path the API does not return — usually the wrong root (a `.body` segment, when the parsed body is bound directly to `${response}`) or the wrong shape | Run `noodle tools call <name> --args <json>` with the secret set and compare the mapped result to the API’s real JSON; map from `${response.<path>}` (the body is `${response}`, there is no `.body`) and use bracket array indices (`${response.items[0].id}`) |
|
|
29
|
+
| A connector should return a list but returns one item, `undefined`, or the whole raw objects | A `${response.arr[0]…}` mapping picks ONE element; a response mapping cannot reshape array items and a tool’s Zod output does not strip them at runtime | Bind the whole array with `${response.<arr>}`, then narrow each element in a compute connector (`references/connect-an-api.md` → “Return a list”) |
|
|
30
|
+
| `noodle dev` boots but the loopback returns `-32600 "not found"` (or 404) for a valid server | A required `secret(...)` is unresolved — a missing secret fails compile *closed* at boot so nothing is served; the local secret was set at a scope `noodle dev` does not read | Set the secret at the scope your local `noodle dev` resolves — `noodle secrets set NAME --runtime local --scope org --org local --from-env NAME`; the clear `missing_secret` line is in the `noodle dev` boot log, not the HTTP response (`references/connect-an-api.md` → “Set the secret for local runs”) |
|
|
29
31
|
| Need to invoke a tool from the terminal | Local tools run in-process; the `noodle` CLI is not a general MCP client for **deployed** URLs (there is no `call <url>` verb) | Locally, `noodle tools call <name> --args <json>` (also `noodle resources read` / `noodle prompts get`) runs the tool against the in-process runtime — with the secret set it executes the connector against the real API, so use it to prove mapped output. For a **deployed** URL use MCP Inspector or `npx @mcpjam/cli@latest tools call --url <url> ...` |
|
|
30
32
|
| One customer/session reports a bad answer or protocol error | The failure may be a model/tool error, host protocol error, or connector/runtime error | Run `noodle metrics --agent-output`, then `noodle events --tool <name> --status tool_error --json`; copy the `sessionId` into `noodle events --session <id> --json`, then match timestamps with `noodle logs` |
|