@noodleseed/agent-kit 0.10.0 → 0.12.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 +11 -11
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +7 -6
- package/skills/claude-code/references/authoring-workflow.md +4 -4
- package/skills/claude-code/references/sdk-surface.md +67 -9
- package/skills/claude-code/references/troubleshooting.md +2 -1
- package/skills/claude-code/references/widgets-and-apps.md +2 -1
- package/skills/codex/SKILL.md +7 -6
- package/skills/codex/references/authoring-workflow.md +4 -4
- package/skills/codex/references/sdk-surface.md +67 -9
- package/skills/codex/references/troubleshooting.md +2 -1
- package/skills/codex/references/widgets-and-apps.md +2 -1
package/manifest.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "0.
|
|
2
|
+
"packageVersion": "0.12.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": "skills/codex/SKILL.md",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "0b986b87d14e9eed8add2c3d294a9c3df319847f410340815d9f4690af126c11",
|
|
7
7
|
"agentTarget": "codex"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
10
10
|
"path": "skills/codex/references/sdk-surface.md",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "410fe309a6091b469892edc55c8083b0643cae7706b3769e9b836546a6ad1235",
|
|
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": "aba1853b5709cc40f341f827bae739e11142b2a6996872757b1db68e0cb35015",
|
|
32
32
|
"agentTarget": "codex"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"path": "skills/codex/references/widgets-and-apps.md",
|
|
36
|
-
"sha256": "
|
|
36
|
+
"sha256": "dea06536f780f0bb6d26dc63a6f1f35e2d6eb97f7a9c1c8574ed7377ccfe37a8",
|
|
37
37
|
"agentTarget": "codex"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
},
|
|
44
44
|
{
|
|
45
45
|
"path": "skills/codex/references/troubleshooting.md",
|
|
46
|
-
"sha256": "
|
|
46
|
+
"sha256": "fe13045fab55fc660c50482540e89654bac6a925c3d189fa3e9650d7e656b7ba",
|
|
47
47
|
"agentTarget": "codex"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
@@ -63,12 +63,12 @@
|
|
|
63
63
|
},
|
|
64
64
|
{
|
|
65
65
|
"path": "skills/claude-code/SKILL.md",
|
|
66
|
-
"sha256": "
|
|
66
|
+
"sha256": "909001bcf63868d51228d2c55da471c12f770b83f15958bbb9eefedb6afd0ec9",
|
|
67
67
|
"agentTarget": "claude-code"
|
|
68
68
|
},
|
|
69
69
|
{
|
|
70
70
|
"path": "skills/claude-code/references/sdk-surface.md",
|
|
71
|
-
"sha256": "
|
|
71
|
+
"sha256": "410fe309a6091b469892edc55c8083b0643cae7706b3769e9b836546a6ad1235",
|
|
72
72
|
"agentTarget": "claude-code"
|
|
73
73
|
},
|
|
74
74
|
{
|
|
@@ -88,12 +88,12 @@
|
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"path": "skills/claude-code/references/authoring-workflow.md",
|
|
91
|
-
"sha256": "
|
|
91
|
+
"sha256": "aba1853b5709cc40f341f827bae739e11142b2a6996872757b1db68e0cb35015",
|
|
92
92
|
"agentTarget": "claude-code"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"path": "skills/claude-code/references/widgets-and-apps.md",
|
|
96
|
-
"sha256": "
|
|
96
|
+
"sha256": "dea06536f780f0bb6d26dc63a6f1f35e2d6eb97f7a9c1c8574ed7377ccfe37a8",
|
|
97
97
|
"agentTarget": "claude-code"
|
|
98
98
|
},
|
|
99
99
|
{
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
},
|
|
104
104
|
{
|
|
105
105
|
"path": "skills/claude-code/references/troubleshooting.md",
|
|
106
|
-
"sha256": "
|
|
106
|
+
"sha256": "fe13045fab55fc660c50482540e89654bac6a925c3d189fa3e9650d7e656b7ba",
|
|
107
107
|
"agentTarget": "claude-code"
|
|
108
108
|
},
|
|
109
109
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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.12.0
|
|
5
|
+
hash: dd621716fbd94dab
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Noodle Seed
|
|
@@ -20,10 +20,11 @@ This CLI is agent-native: the cold-agent-path commands speak the `--json` envelo
|
|
|
20
20
|
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[]`.
|
|
21
21
|
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`).
|
|
22
22
|
5. **Smoke** — `noodle test --json`: local compile plus a loopback MCP smoke.
|
|
23
|
-
6. **
|
|
24
|
-
7. **
|
|
25
|
-
8. **
|
|
26
|
-
9. **
|
|
23
|
+
6. **Prove real output** — `validate`/`test` prove a connector tool *compiles and registers*, not that its response mapping returns data. Set the secret locally (`noodle secrets set <NAME> --from-env <ENV>`), 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.
|
|
24
|
+
7. **Apps/widgets** — `noodle check --json` (add `--target chatgpt|claude`), then `noodle devtools` (see `references/widgets-and-apps.md`).
|
|
25
|
+
8. **Deploy** — `noodle deploy`; auth fails clean with `error.next` = `noodle login` (see `references/deploy-and-ops.md`).
|
|
26
|
+
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`).
|
|
27
|
+
10. **Health** — `noodle metrics --agent-output`: a health verdict plus the exact next command per attention item.
|
|
27
28
|
|
|
28
29
|
## References
|
|
29
30
|
|
|
@@ -36,7 +36,7 @@ Declare connectors as data, not imperative code:
|
|
|
36
36
|
|
|
37
37
|
Tools record connector calls into a flow; recording is not execution. Do not branch on runtime outputs with native `if` — use declarative `when(...)` conditions.
|
|
38
38
|
|
|
39
|
-
HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_TOKEN") }`), `apiKey` (`{ kind: "apiKey", header: "X-API-Key", secret: secret("API_KEY") }`), `
|
|
39
|
+
HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_TOKEN") }`), `apiKey` (`{ kind: "apiKey", header: "X-API-Key", secret: secret("API_KEY") }`), `clientCredentials`, `delegatedOAuth`, and `delegatedSessionCookie`. Use managed `secret(...)` / `variable(...)` refs for all values that differ by org/app/env.
|
|
40
40
|
|
|
41
41
|
## HTTP connector example
|
|
42
42
|
|
|
@@ -72,7 +72,7 @@ const crm = connector('crm').version('1.0.0').http({
|
|
|
72
72
|
});
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Use `${args.field}` for tool/operation inputs and `${response.body.path}` for response mapping. Prefer explicit `allowedOrigins`; OAuth token/session URLs must also be on an allowed origin. For client credentials use `{ kind: "
|
|
75
|
+
Use `${args.field}` for tool/operation inputs and `${response.body.path}` for response mapping. Prefer explicit `allowedOrigins`; OAuth token/session URLs must also be on an allowed origin. For client credentials use `{ kind: "clientCredentials", tokenUrl, clientId, clientSecret, scopes? }` (RFC-6749 grant); for a non-standard partner token endpoint add `profile: "custom"` with a `custom: { requestFormat, clientIdField, clientSecretField, tokenResponsePath, expirySource }` descriptor. Do not put credential headers in operation `headers`; use connector `auth`. For per-customer downstream calls use `delegatedOAuth` or `delegatedSessionCookie` with a matching `customerAuth` server option.
|
|
76
76
|
|
|
77
77
|
## Worked example (full server)
|
|
78
78
|
|
|
@@ -112,7 +112,7 @@ export default server('support', { title: 'Support', version: '1.0.0', use: { cr
|
|
|
112
112
|
]);
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `
|
|
115
|
+
More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials` | `delegatedOAuth` | `delegatedSessionCookie`. Use `.compute(name, { input, output, run })` for a sandboxed transform; `provides:` (instead of `use:`) exposes a connector only to compute `callOperation`; and `noodle import openapi <file>` generates a connector from an OpenAPI spec.
|
|
116
116
|
|
|
117
117
|
## Compute connector example
|
|
118
118
|
|
|
@@ -129,7 +129,7 @@ const scoring = connector('scoring').version('1.0.0').compute('normalize', {
|
|
|
129
129
|
});
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
Compute `run` functions are serialized and sandboxed: no imports, no closure capture, no `fetch`, no `process`. Any backing-system call must be declared in `calls` and invoked through `callOperation`. For conditional flow edges, use `when(...)` in recorded fulfilment instead of native branching on connector outputs.
|
|
132
|
+
Compute `run` functions are serialized and sandboxed: no imports, no closure capture, no `fetch`, no `process`. Any backing-system call must be declared in acyclic `calls` and invoked through `callOperation`. For conditional flow edges, use `when(...)` in recorded fulfilment instead of native branching on connector outputs.
|
|
133
133
|
|
|
134
134
|
## Tests
|
|
135
135
|
|
|
@@ -68,7 +68,7 @@ Minimal, complete, compiling recipes — author in `src/server.ts`, then `noodle
|
|
|
68
68
|
|
|
69
69
|
### Resource
|
|
70
70
|
|
|
71
|
-
`resource(name, { uri, title?, description?, mimeType?, fulfil })`. `fulfil` returns
|
|
71
|
+
`resource(name, { uri, title?, description?, mimeType?, fulfil })`. `fulfil` returns the resource body itself — a plain string, or a bare content entry `{ uri, mimeType, text }` — and the runtime maps it into MCP `contents` for you. Do **not** return a `{ contents: [...] }` wrapper: the runtime already wraps it, so that double-wraps (the whole JSON ends up inside `contents[0].text`). Use a fixed URI for a constant document, or a `{var}` template whose variable arrives on `ctx.input`.
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
74
|
import { resource } from '@noodleseed/one';
|
|
@@ -78,11 +78,8 @@ resource('changelog', {
|
|
|
78
78
|
uri: 'docs://changelog',
|
|
79
79
|
title: 'Changelog',
|
|
80
80
|
mimeType: 'text/markdown',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{ uri: 'docs://changelog', mimeType: 'text/markdown', text: 'Changelog: 1.0.0 first release' },
|
|
84
|
-
],
|
|
85
|
-
}),
|
|
81
|
+
// Return the bare content entry (or just a string); never a { contents: [...] } wrapper.
|
|
82
|
+
fulfil: () => ({ uri: 'docs://changelog', mimeType: 'text/markdown', text: 'Changelog: 1.0.0 first release' }),
|
|
86
83
|
});
|
|
87
84
|
|
|
88
85
|
// {var} URI-template resource: the URI variable arrives on ctx.input (a symbolic ref).
|
|
@@ -91,9 +88,9 @@ resource('ticket', {
|
|
|
91
88
|
title: 'Support ticket',
|
|
92
89
|
mimeType: 'text/markdown',
|
|
93
90
|
fulfil: (ctx) => ({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
uri: `tickets://${ctx.input.id}`,
|
|
92
|
+
mimeType: 'text/markdown',
|
|
93
|
+
text: `Ticket ${ctx.input.id}`,
|
|
97
94
|
}),
|
|
98
95
|
});
|
|
99
96
|
```
|
|
@@ -177,4 +174,65 @@ export default server('support', { title: 'Support', version: '1.0.0', use: { cr
|
|
|
177
174
|
fulfil: async ({ input }) => ({ echo: input.text }),
|
|
178
175
|
}),
|
|
179
176
|
]);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Conditional flow with when()
|
|
180
|
+
|
|
181
|
+
`when(condition, () => record)` records the inner step(s) guarded by a condition instead of a native `if`. `when` is a **free function** (import it), the condition is `ref.equals(scalar)` (equality only — no `<`/`>`/`&&`), and the recorded step is skipped at runtime unless the condition holds. Never write a native `if` on a symbolic ref, and never call a method on one (e.g. `input.name.trim()`) — both silently mis-record or throw; compose strings with a template literal and branch with `when(...)`.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { connector, server, tool, when, z } from '@noodleseed/one';
|
|
185
|
+
|
|
186
|
+
// Two read operations; the tracking lookup only runs when the order came back shipped.
|
|
187
|
+
const orders = connector('orders')
|
|
188
|
+
.version('1.0.0')
|
|
189
|
+
.http({
|
|
190
|
+
baseUrl: 'https://orders.example.com',
|
|
191
|
+
allowedOrigins: ['https://orders.example.com'],
|
|
192
|
+
operations: {
|
|
193
|
+
get_order: {
|
|
194
|
+
type: 'read',
|
|
195
|
+
method: 'GET',
|
|
196
|
+
path: '/orders',
|
|
197
|
+
query: ['id'],
|
|
198
|
+
input: { id: { type: 'string', required: true } },
|
|
199
|
+
output: { id: { type: 'string' }, status: { type: 'string' } },
|
|
200
|
+
response: { id: '${response.id}', status: '${response.status}' },
|
|
201
|
+
},
|
|
202
|
+
get_tracking: {
|
|
203
|
+
type: 'read',
|
|
204
|
+
method: 'GET',
|
|
205
|
+
path: '/tracking',
|
|
206
|
+
query: ['order_id'],
|
|
207
|
+
input: { order_id: { type: 'string', required: true } },
|
|
208
|
+
output: { url: { type: 'string' } },
|
|
209
|
+
response: { url: '${response.url}' },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export default server('orders_app', { title: 'Orders', version: '1.0.0', use: { orders } }, [
|
|
215
|
+
tool('track_order', {
|
|
216
|
+
description: 'Find shipment tracking for an order.',
|
|
217
|
+
input: z.object({ orderId: z.string() }),
|
|
218
|
+
output: z.object({
|
|
219
|
+
orderId: z.string(),
|
|
220
|
+
status: z.string(),
|
|
221
|
+
trackingUrl: z.string().optional(),
|
|
222
|
+
}),
|
|
223
|
+
fulfil: ({ input, connectors }) => {
|
|
224
|
+
const order = connectors.orders.get_order({ id: input.orderId });
|
|
225
|
+
// Record the tracking step only when order.status === "shipped" (equality-only condition).
|
|
226
|
+
const tracking = when(order.status.equals('shipped'), () =>
|
|
227
|
+
connectors.orders.get_tracking({ order_id: order.id }),
|
|
228
|
+
);
|
|
229
|
+
return {
|
|
230
|
+
orderId: order.id,
|
|
231
|
+
status: order.status,
|
|
232
|
+
// `.optional()` marks a ref that may be absent when its guarding step did not run.
|
|
233
|
+
trackingUrl: tracking.url.optional(),
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
]);
|
|
180
238
|
```
|
|
@@ -25,5 +25,6 @@ For protocol/conformance checks, the headless harness is `@mcpjam/cli`, not a `n
|
|
|
25
25
|
| `noodle validate` passes but React views fail to bundle (“requires Vite”) | Project dependencies are not installed — widget bundling uses the app-local Vite | Run the project’s package install, then retry `noodle dev` / `noodle deploy` |
|
|
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 (`${response.body...}` when the parsed body is bound 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
|
+
| 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> ...` |
|
|
29
30
|
| 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` |
|
|
@@ -23,7 +23,8 @@ Author views as React components. `generateHelpers<ServerDefinition>()` (from `@
|
|
|
23
23
|
| `useToolInfo` | Read the invoking tool result; `structuredContent` is the widget’s typed data payload. |
|
|
24
24
|
| `useCallTool` | Call a tool from the widget — returns `{ status, callTool, callToolAsync, data, structuredContent, error, reset }`; target a model-visible tool or a hidden `toolForWidget` helper. |
|
|
25
25
|
| `useViewState` | Persist per-widget UI state across re-renders and restores: `const [value, setValue] = useViewState("key", initial)`. |
|
|
26
|
-
| `useLayout` | Read host layout: `{ theme, displayMode, locale? }` (`
|
|
26
|
+
| `useLayout` | Read host layout: `{ theme, displayMode, locale?, host?, supports? }` (`displayMode` is `"inline"`/`"pip"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
|
|
27
|
+
| `useRequestDisplayMode` | Request a host-mediated layout change such as fullscreen; treat it as best-effort and keep inline rendering useful. |
|
|
27
28
|
| `useOpenExternal` | Open an external link through the host (never `window.open`); the target origin must be listed in the server-level `handoff.allowedDomains`. |
|
|
28
29
|
| `useSendFollowUpMessage` | Send a follow-up prompt to the model from a user interaction: `send({ prompt })` — trigger only from an explicit user action. |
|
|
29
30
|
| `useAppFlow` | Manage named widget views with persisted params and back-stack state: `const flow = useAppFlow({ initialView, views })`. |
|
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.12.0
|
|
5
|
+
hash: 68c2cc25462f5be7
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Noodle Seed
|
|
@@ -20,10 +20,11 @@ This CLI is agent-native: the cold-agent-path commands speak the `--json` envelo
|
|
|
20
20
|
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[]`.
|
|
21
21
|
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`).
|
|
22
22
|
5. **Smoke** — `noodle test --json`: local compile plus a loopback MCP smoke.
|
|
23
|
-
6. **
|
|
24
|
-
7. **
|
|
25
|
-
8. **
|
|
26
|
-
9. **
|
|
23
|
+
6. **Prove real output** — `validate`/`test` prove a connector tool *compiles and registers*, not that its response mapping returns data. Set the secret locally (`noodle secrets set <NAME> --from-env <ENV>`), 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.
|
|
24
|
+
7. **Apps/widgets** — `noodle check --json` (add `--target chatgpt|claude`), then `noodle devtools` (see `references/widgets-and-apps.md`).
|
|
25
|
+
8. **Deploy** — `noodle deploy`; auth fails clean with `error.next` = `noodle login` (see `references/deploy-and-ops.md`).
|
|
26
|
+
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`).
|
|
27
|
+
10. **Health** — `noodle metrics --agent-output`: a health verdict plus the exact next command per attention item.
|
|
27
28
|
|
|
28
29
|
## References
|
|
29
30
|
|
|
@@ -36,7 +36,7 @@ Declare connectors as data, not imperative code:
|
|
|
36
36
|
|
|
37
37
|
Tools record connector calls into a flow; recording is not execution. Do not branch on runtime outputs with native `if` — use declarative `when(...)` conditions.
|
|
38
38
|
|
|
39
|
-
HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_TOKEN") }`), `apiKey` (`{ kind: "apiKey", header: "X-API-Key", secret: secret("API_KEY") }`), `
|
|
39
|
+
HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_TOKEN") }`), `apiKey` (`{ kind: "apiKey", header: "X-API-Key", secret: secret("API_KEY") }`), `clientCredentials`, `delegatedOAuth`, and `delegatedSessionCookie`. Use managed `secret(...)` / `variable(...)` refs for all values that differ by org/app/env.
|
|
40
40
|
|
|
41
41
|
## HTTP connector example
|
|
42
42
|
|
|
@@ -72,7 +72,7 @@ const crm = connector('crm').version('1.0.0').http({
|
|
|
72
72
|
});
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
Use `${args.field}` for tool/operation inputs and `${response.body.path}` for response mapping. Prefer explicit `allowedOrigins`; OAuth token/session URLs must also be on an allowed origin. For client credentials use `{ kind: "
|
|
75
|
+
Use `${args.field}` for tool/operation inputs and `${response.body.path}` for response mapping. Prefer explicit `allowedOrigins`; OAuth token/session URLs must also be on an allowed origin. For client credentials use `{ kind: "clientCredentials", tokenUrl, clientId, clientSecret, scopes? }` (RFC-6749 grant); for a non-standard partner token endpoint add `profile: "custom"` with a `custom: { requestFormat, clientIdField, clientSecretField, tokenResponsePath, expirySource }` descriptor. Do not put credential headers in operation `headers`; use connector `auth`. For per-customer downstream calls use `delegatedOAuth` or `delegatedSessionCookie` with a matching `customerAuth` server option.
|
|
76
76
|
|
|
77
77
|
## Worked example (full server)
|
|
78
78
|
|
|
@@ -112,7 +112,7 @@ export default server('support', { title: 'Support', version: '1.0.0', use: { cr
|
|
|
112
112
|
]);
|
|
113
113
|
```
|
|
114
114
|
|
|
115
|
-
More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `
|
|
115
|
+
More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials` | `delegatedOAuth` | `delegatedSessionCookie`. Use `.compute(name, { input, output, run })` for a sandboxed transform; `provides:` (instead of `use:`) exposes a connector only to compute `callOperation`; and `noodle import openapi <file>` generates a connector from an OpenAPI spec.
|
|
116
116
|
|
|
117
117
|
## Compute connector example
|
|
118
118
|
|
|
@@ -129,7 +129,7 @@ const scoring = connector('scoring').version('1.0.0').compute('normalize', {
|
|
|
129
129
|
});
|
|
130
130
|
```
|
|
131
131
|
|
|
132
|
-
Compute `run` functions are serialized and sandboxed: no imports, no closure capture, no `fetch`, no `process`. Any backing-system call must be declared in `calls` and invoked through `callOperation`. For conditional flow edges, use `when(...)` in recorded fulfilment instead of native branching on connector outputs.
|
|
132
|
+
Compute `run` functions are serialized and sandboxed: no imports, no closure capture, no `fetch`, no `process`. Any backing-system call must be declared in acyclic `calls` and invoked through `callOperation`. For conditional flow edges, use `when(...)` in recorded fulfilment instead of native branching on connector outputs.
|
|
133
133
|
|
|
134
134
|
## Tests
|
|
135
135
|
|
|
@@ -68,7 +68,7 @@ Minimal, complete, compiling recipes — author in `src/server.ts`, then `noodle
|
|
|
68
68
|
|
|
69
69
|
### Resource
|
|
70
70
|
|
|
71
|
-
`resource(name, { uri, title?, description?, mimeType?, fulfil })`. `fulfil` returns
|
|
71
|
+
`resource(name, { uri, title?, description?, mimeType?, fulfil })`. `fulfil` returns the resource body itself — a plain string, or a bare content entry `{ uri, mimeType, text }` — and the runtime maps it into MCP `contents` for you. Do **not** return a `{ contents: [...] }` wrapper: the runtime already wraps it, so that double-wraps (the whole JSON ends up inside `contents[0].text`). Use a fixed URI for a constant document, or a `{var}` template whose variable arrives on `ctx.input`.
|
|
72
72
|
|
|
73
73
|
```ts
|
|
74
74
|
import { resource } from '@noodleseed/one';
|
|
@@ -78,11 +78,8 @@ resource('changelog', {
|
|
|
78
78
|
uri: 'docs://changelog',
|
|
79
79
|
title: 'Changelog',
|
|
80
80
|
mimeType: 'text/markdown',
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
{ uri: 'docs://changelog', mimeType: 'text/markdown', text: 'Changelog: 1.0.0 first release' },
|
|
84
|
-
],
|
|
85
|
-
}),
|
|
81
|
+
// Return the bare content entry (or just a string); never a { contents: [...] } wrapper.
|
|
82
|
+
fulfil: () => ({ uri: 'docs://changelog', mimeType: 'text/markdown', text: 'Changelog: 1.0.0 first release' }),
|
|
86
83
|
});
|
|
87
84
|
|
|
88
85
|
// {var} URI-template resource: the URI variable arrives on ctx.input (a symbolic ref).
|
|
@@ -91,9 +88,9 @@ resource('ticket', {
|
|
|
91
88
|
title: 'Support ticket',
|
|
92
89
|
mimeType: 'text/markdown',
|
|
93
90
|
fulfil: (ctx) => ({
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
uri: `tickets://${ctx.input.id}`,
|
|
92
|
+
mimeType: 'text/markdown',
|
|
93
|
+
text: `Ticket ${ctx.input.id}`,
|
|
97
94
|
}),
|
|
98
95
|
});
|
|
99
96
|
```
|
|
@@ -177,4 +174,65 @@ export default server('support', { title: 'Support', version: '1.0.0', use: { cr
|
|
|
177
174
|
fulfil: async ({ input }) => ({ echo: input.text }),
|
|
178
175
|
}),
|
|
179
176
|
]);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Conditional flow with when()
|
|
180
|
+
|
|
181
|
+
`when(condition, () => record)` records the inner step(s) guarded by a condition instead of a native `if`. `when` is a **free function** (import it), the condition is `ref.equals(scalar)` (equality only — no `<`/`>`/`&&`), and the recorded step is skipped at runtime unless the condition holds. Never write a native `if` on a symbolic ref, and never call a method on one (e.g. `input.name.trim()`) — both silently mis-record or throw; compose strings with a template literal and branch with `when(...)`.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { connector, server, tool, when, z } from '@noodleseed/one';
|
|
185
|
+
|
|
186
|
+
// Two read operations; the tracking lookup only runs when the order came back shipped.
|
|
187
|
+
const orders = connector('orders')
|
|
188
|
+
.version('1.0.0')
|
|
189
|
+
.http({
|
|
190
|
+
baseUrl: 'https://orders.example.com',
|
|
191
|
+
allowedOrigins: ['https://orders.example.com'],
|
|
192
|
+
operations: {
|
|
193
|
+
get_order: {
|
|
194
|
+
type: 'read',
|
|
195
|
+
method: 'GET',
|
|
196
|
+
path: '/orders',
|
|
197
|
+
query: ['id'],
|
|
198
|
+
input: { id: { type: 'string', required: true } },
|
|
199
|
+
output: { id: { type: 'string' }, status: { type: 'string' } },
|
|
200
|
+
response: { id: '${response.id}', status: '${response.status}' },
|
|
201
|
+
},
|
|
202
|
+
get_tracking: {
|
|
203
|
+
type: 'read',
|
|
204
|
+
method: 'GET',
|
|
205
|
+
path: '/tracking',
|
|
206
|
+
query: ['order_id'],
|
|
207
|
+
input: { order_id: { type: 'string', required: true } },
|
|
208
|
+
output: { url: { type: 'string' } },
|
|
209
|
+
response: { url: '${response.url}' },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
export default server('orders_app', { title: 'Orders', version: '1.0.0', use: { orders } }, [
|
|
215
|
+
tool('track_order', {
|
|
216
|
+
description: 'Find shipment tracking for an order.',
|
|
217
|
+
input: z.object({ orderId: z.string() }),
|
|
218
|
+
output: z.object({
|
|
219
|
+
orderId: z.string(),
|
|
220
|
+
status: z.string(),
|
|
221
|
+
trackingUrl: z.string().optional(),
|
|
222
|
+
}),
|
|
223
|
+
fulfil: ({ input, connectors }) => {
|
|
224
|
+
const order = connectors.orders.get_order({ id: input.orderId });
|
|
225
|
+
// Record the tracking step only when order.status === "shipped" (equality-only condition).
|
|
226
|
+
const tracking = when(order.status.equals('shipped'), () =>
|
|
227
|
+
connectors.orders.get_tracking({ order_id: order.id }),
|
|
228
|
+
);
|
|
229
|
+
return {
|
|
230
|
+
orderId: order.id,
|
|
231
|
+
status: order.status,
|
|
232
|
+
// `.optional()` marks a ref that may be absent when its guarding step did not run.
|
|
233
|
+
trackingUrl: tracking.url.optional(),
|
|
234
|
+
};
|
|
235
|
+
},
|
|
236
|
+
}),
|
|
237
|
+
]);
|
|
180
238
|
```
|
|
@@ -25,5 +25,6 @@ For protocol/conformance checks, the headless harness is `@mcpjam/cli`, not a `n
|
|
|
25
25
|
| `noodle validate` passes but React views fail to bundle (“requires Vite”) | Project dependencies are not installed — widget bundling uses the app-local Vite | Run the project’s package install, then retry `noodle dev` / `noodle deploy` |
|
|
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 (`${response.body...}` when the parsed body is bound 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
|
+
| 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> ...` |
|
|
29
30
|
| 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` |
|
|
@@ -23,7 +23,8 @@ Author views as React components. `generateHelpers<ServerDefinition>()` (from `@
|
|
|
23
23
|
| `useToolInfo` | Read the invoking tool result; `structuredContent` is the widget’s typed data payload. |
|
|
24
24
|
| `useCallTool` | Call a tool from the widget — returns `{ status, callTool, callToolAsync, data, structuredContent, error, reset }`; target a model-visible tool or a hidden `toolForWidget` helper. |
|
|
25
25
|
| `useViewState` | Persist per-widget UI state across re-renders and restores: `const [value, setValue] = useViewState("key", initial)`. |
|
|
26
|
-
| `useLayout` | Read host layout: `{ theme, displayMode, locale? }` (`
|
|
26
|
+
| `useLayout` | Read host layout: `{ theme, displayMode, locale?, host?, supports? }` (`displayMode` is `"inline"`/`"pip"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
|
|
27
|
+
| `useRequestDisplayMode` | Request a host-mediated layout change such as fullscreen; treat it as best-effort and keep inline rendering useful. |
|
|
27
28
|
| `useOpenExternal` | Open an external link through the host (never `window.open`); the target origin must be listed in the server-level `handoff.allowedDomains`. |
|
|
28
29
|
| `useSendFollowUpMessage` | Send a follow-up prompt to the model from a user interaction: `send({ prompt })` — trigger only from an explicit user action. |
|
|
29
30
|
| `useAppFlow` | Manage named widget views with persisted params and back-stack state: `const flow = useAppFlow({ initialView, views })`. |
|