@noodleseed/agent-kit 0.9.0 → 0.11.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.
@@ -3,60 +3,207 @@
3
3
  ## Contents
4
4
 
5
5
  - Tools and views
6
- - React authoring
7
6
  - React hook surface
8
- - Shaping tool output for widgets
9
- - Widget state and model context
10
- - CSP, domain, and permissions
11
- - Host bridge
7
+ - Worked widget recipe
8
+ - ChatGPT App = this widget + a domain
9
+ - Knowledge/source apps (search + fetch)
10
+ - Permissions and host bridge
12
11
  - Readiness and boundaries
13
12
 
14
13
  ## Tools and views
15
14
 
16
- Use `toolWithWidget(name, { description, input, output, fulfil, view })` for a model-visible tool that renders a widget, and `toolForWidget(name, { ... })` for a widget-only helper hidden from the model. A `view` is `{ component: "name", entry: "./views/name.tsx" }`.
17
-
18
- ## React authoring
19
-
20
- Author views as React components. Get typed helpers from `@noodleseed/one/react`:
21
-
22
- ```ts
23
- import { generateHelpers } from '@noodleseed/one/react';
24
- const { useToolInfo, useCallTool, useViewState, useLayout, useOpenExternal, useSendFollowUpMessage } =
25
- generateHelpers<AppType>();
26
- ```
27
-
28
- Bind interactive elements to tools (`useCallTool("place_order")`) and annotate model-facing context with `data-llm`. A raw `html` escape hatch exists for self-contained widgets (declarative `data-bind`/`data-action`; no inline `<script>`).
15
+ Use `toolWithWidget(name, { description, input, output, fulfil, view })` for a model-visible tool that renders a widget, and `toolForWidget(name, { ... })` for a widget-only helper hidden from the model. A `view` is `{ component: "name", entry: "./views/name.tsx" }` — a React component the compiler bundles at validate/deploy time.
29
16
 
30
17
  ## React hook surface
31
18
 
19
+ Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
20
+
32
21
  | Hook | Use for |
33
22
  | :-- | :-- |
34
23
  | `useToolInfo` | Read the invoking tool result; `structuredContent` is the widget’s typed data payload. |
35
- | `useCallTool` | Call a tool from the widget — returns `{ callTool, data, error, isPending }`; target a model-visible tool or a hidden `toolForWidget` helper. |
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. |
36
25
  | `useViewState` | Persist per-widget UI state across re-renders and restores: `const [value, setValue] = useViewState("key", initial)`. |
37
- | `useLayout` | Read host layout: `{ theme, displayMode, locale? }` (`theme` is `"light"`/`"dark"`, `displayMode` is `"inline"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
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. |
38
28
  | `useOpenExternal` | Open an external link through the host (never `window.open`); the target origin must be listed in the server-level `handoff.allowedDomains`. |
39
29
  | `useSendFollowUpMessage` | Send a follow-up prompt to the model from a user interaction: `send({ prompt })` — trigger only from an explicit user action. |
30
+ | `useAppFlow` | Manage named widget views with persisted params and back-stack state: `const flow = useAppFlow({ initialView, views })`. |
31
+ | `useHandoff` | Open server-created HTTP(S) handoff URLs through the host with status/error state; domain policy still comes from `handoff.allowedDomains`. |
32
+
33
+ Bind interactive elements to tools (`useCallTool("place_order")`), drive named views with `useAppFlow(...)`, open server-created handoffs with `useHandoff()`, and annotate model-facing context with `data-llm`. Use `createViewStore("key", initial)` for multi-component widget state such as carts, filters, or drafts. Use the domain-neutral React components from `@noodleseed/one/react` (`AppShell`, `ShellNav`, `ViewStack`, `AsyncBoundary`, `ActionBar`, `Field`, `QuantityStepper`, `ChoiceGroup`, `HandoffButton`, and related state components) for rich apps before inventing local shell/control scaffolding. Adapt to the host with `useLayout()` — style for both `theme` values, and keep the inline `displayMode` compact (content fits the space; no internal scrolling). Trigger `useOpenExternal()`, `useHandoff()`, and `useSendFollowUpMessage()` only from explicit user actions. A raw `html` escape hatch exists for self-contained widgets (declarative `data-bind`/`data-action`; no inline `<script>`).
34
+
35
+ ## Worked widget recipe
36
+
37
+ Minimal, complete, and compile-verified — `noodle validate` bundles the view and `noodle check --target chatgpt` audits it. Author two files: the view (`src/views/order-status.tsx`) and the tool declaration (`src/server.ts`).
38
+
39
+ ### 1. The view component
40
+
41
+ Author React. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks: read the tool result with `useToolInfo`, call a widget-only helper with `useCallTool`, keep local UI state that survives re-render with `useViewState`, open an allowlisted link with `useOpenExternal`, and mirror model-facing context back to the model as text with `data-llm`.
40
42
 
41
- Adapt to the host with `useLayout()` — style for both `theme` values, and keep the inline `displayMode` compact (content fits the space; no internal scrolling). Trigger `useOpenExternal()` and `useSendFollowUpMessage()` only from explicit user actions.
43
+ ```tsx
44
+ import type { ServerDefinition } from '@noodleseed/one';
45
+ import { generateHelpers } from '@noodleseed/one/react';
46
+
47
+ // One call wires the typed host bridge; destructure only the hooks this view uses.
48
+ const { useToolInfo, useCallTool, useViewState, useOpenExternal } =
49
+ generateHelpers<ServerDefinition>();
50
+
51
+ type OrderResult = {
52
+ readonly customer?: string;
53
+ readonly item?: string;
54
+ readonly total?: number;
55
+ readonly checkoutUrl?: string;
56
+ };
57
+
58
+ export default function OrderStatus() {
59
+ const shown = useToolInfo('show_order').structuredContent as OrderResult | undefined;
60
+ const placeOrder = useCallTool('place_order'); // calls the widget-only helper tool
61
+ const openExternal = useOpenExternal();
62
+ const [item, setItem] = useViewState('item', shown?.item ?? 'falafel_wrap'); // survives re-render
63
+ const confirmed = placeOrder.data?.structuredContent as { readonly status?: string } | undefined;
64
+ const total = shown?.total ?? 0;
65
+ const checkoutUrl = shown?.checkoutUrl ?? '';
66
+
67
+ return (
68
+ // data-llm mirrors the visible state back to the model as text context.
69
+ <main data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
70
+ <h1>Pickup order</h1>
71
+ <label>
72
+ Item
73
+ <select value={item} onChange={(event) => setItem(event.currentTarget.value)}>
74
+ <option value="falafel_wrap">Falafel Wrap</option>
75
+ <option value="lentil_soup">Lentil Soup</option>
76
+ <option value="mint_lemonade">Mint Lemonade</option>
77
+ </select>
78
+ </label>
79
+ <button
80
+ type="button"
81
+ disabled={placeOrder.isPending}
82
+ onClick={() => placeOrder.callTool({ customer: shown?.customer ?? 'Guest', item })}
83
+ >
84
+ {placeOrder.isPending ? 'Placing…' : 'Place order'}
85
+ </button>
86
+ <p>{confirmed?.status ?? `Total: $${total}`}</p>
87
+ <button type="button" onClick={() => openExternal(checkoutUrl)}>
88
+ Continue checkout
89
+ </button>
90
+ </main>
91
+ );
92
+ }
93
+ ```
94
+
95
+ ### 2. The tool declaration
42
96
 
43
- ## Shaping tool output for widgets
97
+ `toolWithWidget` is the model-visible tool that renders the view; pair it with `toolForWidget` helpers the view calls (hidden from the model). Wire `view: { component, entry }`, `csp`, a widget `domain`, and a real `output` schema so non-Apps hosts still receive structured data. Inside `fulfil`, `input` is a symbolic ref recorded into a flow — reference it in output/template strings, but never use it as an object key or `if` condition.
98
+
99
+ ```ts
100
+ import { annotations, server, toolForWidget, toolWithWidget, z } from '@noodleseed/one';
101
+
102
+ const item = z.enum(['falafel_wrap', 'lentil_soup', 'mint_lemonade']).default('falafel_wrap');
103
+ const checkoutUrl = (customer: string) =>
104
+ `https://orders.example.com/pickup?customer=${customer}`;
105
+
106
+ export default server(
107
+ 'pickup',
108
+ {
109
+ title: 'Pickup',
110
+ version: '1.0.0',
111
+ // External-link targets the widget opens; the compiler derives ChatGPT redirect_domains from this.
112
+ handoff: { allowedDomains: ['https://orders.example.com'] },
113
+ },
114
+ [
115
+ toolWithWidget('show_order', {
116
+ description: 'Show the pickup order and render the ordering widget.',
117
+ // Declare tool annotations — a ChatGPT-submission requirement. A read → `readOnly()`.
118
+ annotations: annotations.readOnly(),
119
+ input: z.object({ customer: z.string().default('Guest') }),
120
+ output: z.object({
121
+ customer: z.string(),
122
+ item: z.string(),
123
+ total: z.number(),
124
+ checkoutUrl: z.string(),
125
+ }),
126
+ fulfil: ({ input }) => ({
127
+ customer: input.customer,
128
+ item: 'falafel_wrap',
129
+ total: 12,
130
+ checkoutUrl: checkoutUrl(input.customer),
131
+ }),
132
+ widgetTitle: 'Pickup order',
133
+ widgetDescription: 'Pick an item and place a pickup order.',
134
+ // A ChatGPT App is this widget + a domain: one https origin per app.
135
+ domain: 'https://pickup.example.com',
136
+ view: { component: 'order-status', entry: './views/order-status.tsx' },
137
+ csp: {
138
+ connectDomains: ['https://orders.example.com'],
139
+ resourceDomains: ['https://example.com'],
140
+ // Keep CSP origins exact and minimal. Add `frameDomains` ONLY if the widget embeds an
141
+ // iframe — it relaxes subframe rendering and triggers stricter ChatGPT review.
142
+ },
143
+ }),
144
+ // Widget-only helper the view calls with useCallTool('place_order'); hidden from the model.
145
+ toolForWidget('place_order', {
146
+ description: 'Place a pickup order from the widget.',
147
+ // A write that reaches the outside world → `action()` (not read-only, not destructive).
148
+ annotations: annotations.action(),
149
+ input: z.object({ customer: z.string().default('Guest'), item }),
150
+ output: z.object({ status: z.string(), item: z.string(), checkoutUrl: z.string() }),
151
+ fulfil: ({ input }) => ({
152
+ status: `Order placed for ${input.customer}.`,
153
+ item: input.item,
154
+ checkoutUrl: checkoutUrl(input.customer),
155
+ }),
156
+ }),
157
+ ],
158
+ );
159
+ ```
44
160
 
45
- The widget reads the tool result’s `structuredContent`, typed by the tool’s `output` schema — arrays and nested objects are fully supported, so model the data naturally (`z.array(z.object({ ... }))`) instead of flattening. Keep the result’s `content` useful on its own: hosts without MCP Apps support show only the text/structured result, so the tool must degrade gracefully.
161
+ ## ChatGPT App = this widget + a domain
46
162
 
47
- ## Widget state and model context
163
+ A "ChatGPT App" is not a separate authoring surface — it is exactly this MCP Apps widget rendered by the ChatGPT host. From the same declaration you author three things:
48
164
 
49
- Persist UI state (selections, drafts, pagination) with `useViewState("key", initial)` it survives re-renders and conversation restores. For durable, caller-scoped state handles with optimistic revisions, follow the `stateful-draft` example. Mark the DOM the model should see with `data-llm` attributes; everything else stays widget-only.
165
+ - `domain` on the widgetone https origin per app (required for app-store submission, optional for dev-mode testing).
166
+ - `csp: { connectDomains, resourceDomains }` — the exact network/resource origins the widget may reach; keep them minimal. Add `frameDomains` only if the widget embeds an iframe (it relaxes subframe rendering and draws stricter review).
167
+ - server `handoff.allowedDomains` — the external-link targets `useOpenExternal()` opens.
50
168
 
51
- ## CSP, domain, and permissions
169
+ The compiler emits the rest automatically: the `openai/*` metadata (`openai/outputTemplate`, `openai/widgetCSP`, `openai/widgetDescription`) and ChatGPT’s `redirect_domains` (derived from `handoff.allowedDomains`). `window.openai` and Claude’s ext-apps bridge are auto-detected at startup, so the same widget renders in both Claude and ChatGPT with no host-specific code.
52
170
 
53
- Declare network/host needs explicitly: `csp: { connectDomains, resourceDomains, frameDomains }` and `permissions` (e.g. `clipboardWrite`). Secrets are never injected into widgets; tool output is redacted before widget delivery.
171
+ Verify it in the loop: `noodle check --target chatgpt --json` returning `ok:true` means the widget is **metadata-ready** for ChatGPT’s checks (`domain`, `openai/outputTemplate`, CSP present) — it does NOT prove host rendering, conversation UX, or submission acceptance. Fix any `severity:"error"` finding by its `fix`, then re-check. Validate real rendering in ChatGPT Developer Mode / MCP Inspector as a higher level before submitting.
172
+
173
+ **Every tool needs annotations** (`annotations.readOnly()` / `.action()` / `.openAction()`) — missing or wrong `readOnlyHint`/`openWorldHint`/`destructiveHint` is a common submission rejection. **App-store submission is more than building**: a public https endpoint, exact CSP, org verification, app info, screenshots, and test prompts are required — follow OpenAI’s Apps submission guidelines; `noodle check --target chatgpt` covers only the metadata prerequisites.
174
+
175
+ ## Knowledge/source apps (ChatGPT): search + fetch
176
+
177
+ If the app is a read-only knowledge/source connector (docs, wiki, CRM lookups) meant for ChatGPT company-knowledge, implement exactly two tools — `search` and `fetch`, both read-only. ChatGPT only surfaces knowledge apps that match these signatures:
178
+
179
+ ```ts
180
+ import { annotations, server, tool, z } from '@noodleseed/one';
181
+
182
+ export default server('kb', { title: 'Knowledge base', version: '1.0.0' }, [
183
+ tool('search', {
184
+ description: 'Search the knowledge base; return citable results.',
185
+ annotations: annotations.readOnly(),
186
+ input: z.object({ query: z.string() }),
187
+ output: z.object({
188
+ results: z.array(z.object({ id: z.string(), title: z.string(), url: z.string() })),
189
+ }),
190
+ fulfil: ({ input }) => ({ results: [{ id: 'doc-1', title: `Match: ${input.query}`, url: 'https://example.com/doc-1' }] }),
191
+ }),
192
+ tool('fetch', {
193
+ description: 'Fetch one document by id for citation.',
194
+ annotations: annotations.readOnly(),
195
+ input: z.object({ id: z.string() }),
196
+ output: z.object({ id: z.string(), title: z.string(), text: z.string(), url: z.string() }),
197
+ fulfil: ({ input }) => ({ id: input.id, title: 'Doc', text: 'Full document text…', url: 'https://example.com/doc-1' }),
198
+ }),
199
+ ]);
200
+ ```
54
201
 
55
- For ChatGPT: set `domain` on each widget (one https origin per app; required for app-store submission, optional for dev-mode testing), and declare external-link targets in the server-level `handoff.allowedDomains` the compiler derives ChatGPT’s `redirect_domains` from it so `useOpenExternal()` links open without the safe-link warning. `noodle check --target chatgpt` verifies all of this.
202
+ `search` `{ results: [{ id, title, url }] }`; `fetch` `{ id, title, text, url, metadata? }`. `url` must be an absolute, user-openable https link for citation.
56
203
 
57
- ## Host bridge
204
+ ## Permissions and host bridge
58
205
 
59
- One runtime targets both Claude’s ext-apps bridge and ChatGPT’s `window.openai`, detected at startup. Tool results still carry useful `content`/`structuredContent` so non-Apps hosts degrade gracefully.
206
+ Declare extra host capabilities with `permissions` (e.g. `permissions: { clipboardWrite: {} }`). Secrets are never injected into widgets and tool output is redacted before widget delivery. Tool results still carry useful `content`/`structuredContent`, so non-Apps hosts degrade gracefully.
60
207
 
61
208
  ## Readiness and boundaries
62
209
 
@@ -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.9.0
5
- hash: 097c37f2e7a3e482
4
+ version: 0.11.0
5
+ hash: 7cae1fbf5bdf6a0d
6
6
  ---
7
7
 
8
8
  # Noodle Seed
@@ -13,17 +13,23 @@ Use this skill for local Noodle Seed project work in Codex.
13
13
 
14
14
  ## Golden path
15
15
 
16
- 1. `noodle validate` author-time compile/schema/connector check. On failure, fix the cited errors (see `references/compile-errors.md`) and re-validate; do not freeform re-edit.
17
- 2. `noodle test` — local compile plus a loopback MCP smoke.
18
- 3. `noodle dev` local loopback runtime that serves and hot-reloads the manifest.
19
- 4. `noodle check` / `noodle devtools` MCP Apps/widget readiness and preview (see `references/widgets-and-apps.md`).
20
- 5. `noodle deploy` after `noodle login` + `noodle link` (see `references/deploy-and-ops.md`).
21
- 6. Prove it in a real host `noodle connect <client>` (see `references/test-in-hosts.md`); debug symptoms with `references/troubleshooting.md`.
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
+
18
+ 1. **Discover** — `noodle commands --json`: every command, subcommand, flag, and exit code (don't read source).
19
+ 2. **Author** edit `src/server.ts` (the configured entrypoint); follow the capability recipe in `references/sdk-surface.md` and `references/examples.md`.
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
+ 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
+ 5. **Smoke** — `noodle test --json`: local compile plus a loopback MCP smoke.
23
+ 6. **Apps/widgets** — `noodle check --json` (add `--target chatgpt|claude`), then `noodle devtools` (see `references/widgets-and-apps.md`).
24
+ 7. **Deploy** — `noodle deploy`; auth fails clean with `error.next` = `noodle login` (see `references/deploy-and-ops.md`).
25
+ 8. **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`).
26
+ 9. **Health** — `noodle metrics --agent-output`: a health verdict plus the exact next command per attention item.
22
27
 
23
28
  ## References
24
29
 
25
30
  Load these on demand:
26
31
 
32
+ - `references/agent-contract.md` — the `--json` envelope, exit codes, and the three output modes.
27
33
  - `references/sdk-surface.md` — what to import from `@noodleseed/one` and which builder to use.
28
34
  - `references/cli-commands.md` — every `noodle` command, grouped by area.
29
35
  - `references/compile-errors.md` — fix `noodle validate` errors by code.
@@ -0,0 +1,43 @@
1
+ # Agent contract: --json, exit codes, output modes
2
+
3
+ The cold-agent-path commands (`init`, `validate`, `test`, `check`, `tools`/`resources`/`prompts`, `deploy`, `metrics`, `events`, `agents`) are agent-native and return the envelope below; hosted admin/ops commands (`status`, `inspect`, `smoke`, `logs`, `update`) are still being normalized. Decide what to do next by parsing machine state — do not scrape human prose.
4
+
5
+ ## Contents
6
+
7
+ - Response envelope
8
+ - Exit codes
9
+ - Output modes
10
+ - Repair loop
11
+
12
+ ## Response envelope
13
+
14
+ A `--json` command returns exactly one JSON object:
15
+
16
+ - **Success**: `{ ok: true, data, warnings? }` — `data` is the command payload; `warnings?` is an optional array of non-fatal notes.
17
+ - **Failure**: `{ ok: false, error: { code, message, cause?, fix, next, requestId? } }` — `code` is the stable machine code to branch on, `message` is human text, `cause?` is the underlying error, `fix` states the correction, `next` names the command to run next, `requestId?` correlates a hosted call.
18
+ - **Field errors** carry a dotted `path`: multi-error commands (e.g. `noodle validate`) nest them under `error.errors[]`, each `{ code, path, message }`. The top-level `error` still carries `code`/`message`/`fix`/`next`; the per-field `path`s live in `error.errors[]`.
19
+ - **Repair prose is isolated**: ready-to-apply repair text appears only under `error.fixPrompt` (surfaced by `--fix-prompt`), never mixed into `message` or `data`.
20
+
21
+ ## Exit codes
22
+
23
+ Branch on the process exit code before parsing the body:
24
+
25
+ | Code | Meaning |
26
+ | :-- | :-- |
27
+ | `0` | ok |
28
+ | `1` | failure (command ran, the work failed) |
29
+ | `2` | usage (bad flags or arguments) |
30
+ | `3` | auth (login or permission required) |
31
+ | `4` | unreachable (service or network) |
32
+ | `5` | mcp/tool-call error (a `tools`/`resources`/`prompts`/`test` smoke call failed) |
33
+
34
+ ## Output modes
35
+
36
+ Two kinds of output — never mix them:
37
+
38
+ - `--json` — **machine state**: the envelope above. Use it to decide what to do next.
39
+ - `--fix-prompt` / `--agent-output` (aliases) — **agent-readable text**, not the envelope: a ready-to-apply repair prompt for authoring commands (`validate`/`test`/`check`), or an operational `health` verdict (`ok`/`attention`) with `attention[]` next-commands for ops commands (`metrics`/`doctor`/`alerts`). Use it to author a fix or judge a running deployment.
40
+
41
+ ## Repair loop
42
+
43
+ On a `validate` failure: parse `error.code` + `path`, fix exactly that field in `src/server.ts`, then re-run `noodle validate --json`. Never freeform re-edit. Repeat until `ok: true`, then `noodle test --json`.
@@ -7,6 +7,7 @@
7
7
  - Repair loop
8
8
  - Connectors
9
9
  - HTTP connector example
10
+ - Worked example (full server)
10
11
  - Compute connector example
11
12
  - Tests
12
13
  - Secrets and variables
@@ -39,6 +40,8 @@ HTTP connector auth variants: `bearer` (`{ kind: "bearer", secret: secret("API_T
39
40
 
40
41
  ## HTTP connector example
41
42
 
43
+ The operation mapping in detail: `request` templates the outbound call, `response` maps the HTTP body into your typed `output`.
44
+
42
45
  ```ts
43
46
  import { connector, secret, variable } from '@noodleseed/one';
44
47
 
@@ -71,6 +74,46 @@ const crm = connector('crm').version('1.0.0').http({
71
74
 
72
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: "oauth2ClientCredentials", tokenUrl, clientId, clientSecret, scopes? }`; for per-customer downstream calls use `delegatedOAuth` or `delegatedSessionCookie` with a matching `customerAuth` server option.
73
76
 
77
+ ## Worked example (full server)
78
+
79
+ Declare the API as data, bind it with `use`, then record a call in a tool. `auth` reads a managed `secret(...)` — never inline a key. Map the HTTP body into your typed `output` with `${response...}`.
80
+
81
+ ```ts
82
+ import { connector, secret, server, tool, z } from '@noodleseed/one';
83
+
84
+ const crm = connector('acme_crm')
85
+ .version('1.0.0')
86
+ .http({
87
+ baseUrl: 'https://api.acme.example',
88
+ allowedOrigins: ['https://api.acme.example'],
89
+ auth: { kind: 'apiKey', header: 'X-Api-Key', secret: secret('ACME_CRM_TOKEN') },
90
+ operations: {
91
+ get_ticket: {
92
+ type: 'read',
93
+ method: 'GET',
94
+ path: '/v1/tickets/{id}',
95
+ input: { id: { type: 'string', required: true } },
96
+ output: { subject: { type: 'string' }, status: { type: 'string' } },
97
+ response: { subject: '${response.data.subject}', status: '${response.data.state}' },
98
+ },
99
+ },
100
+ });
101
+
102
+ export default server('support', { title: 'Support', version: '1.0.0', use: { crm } }, [
103
+ tool('ticket_status', {
104
+ description: 'Look up a support ticket by id.',
105
+ input: z.object({ ticket_id: z.string() }),
106
+ output: z.object({ subject: z.string(), status: z.string() }),
107
+ fulfil: ({ input, connectors }) => {
108
+ const ticket = connectors.crm.get_ticket({ id: input.ticket_id });
109
+ return { subject: ticket.subject, status: ticket.status };
110
+ },
111
+ }),
112
+ ]);
113
+ ```
114
+
115
+ More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `oauth2ClientCredentials` | `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
+
74
117
  ## Compute connector example
75
118
 
76
119
  ```ts
@@ -3,39 +3,11 @@
3
3
  ## Contents
4
4
 
5
5
  - The repair loop
6
- - JSON shape
7
- - Fix prompt
8
6
  - Error codes
9
7
 
10
8
  ## The repair loop
11
9
 
12
- Run `noodle validate` (add `--json` for machine-readable output, `--fix-prompt` for an agent repair prompt). Each error carries a `code`, a dotted `path` to the offending field, and a `message`; many also carry `expected`/`got`, `didYouMean`/`suggestions`, and a `docAnchor`. Fix the specific error the `path` locates, then re-validate. Do not freeform re-edit. Once `noodle validate` passes, run `noodle test`, then `noodle dev`.
13
-
14
- ## JSON shape
15
-
16
- Use `noodle validate --json` in automated repair loops. Treat `errors[]` as the work queue and change only the cited file/path before re-running validation.
17
-
18
- ```json
19
- {
20
- "ok": false,
21
- "errors": [
22
- {
23
- "code": "unknown_operation",
24
- "path": "tools.search.fulfil.steps.0.operation",
25
- "message": "Unknown connector operation.",
26
- "expected": "operation declared on connector alias `crm`",
27
- "got": "crm.find_customer",
28
- "didYouMean": "crm.findCustomer",
29
- "suggestions": ["crm.findCustomer"],
30
- "docAnchor": "connectors.operations"
31
- }
32
- ]
33
- }
34
- ```
35
-
36
- ## Fix prompt
37
-
38
- `noodle validate --fix-prompt` prints a compact agent repair prompt with the same structured errors. Use it when delegating a repair pass, but still inspect the resulting code and re-run `noodle validate --json`.
10
+ Run `noodle validate` (add `--json` for the machine-readable envelope, `--fix-prompt` for an agent repair prompt). On failure the envelope is `{ok:false,error:{code,message,fix,next,errors:[{code,path,message}]}}`: each entry in `error.errors[]` carries a `code`, a dotted `path` to the offending field, and a `message`; many also carry `expected`/`got`, `didYouMean`/`suggestions`, and a `docAnchor` (the full envelope is in `agent-contract.md`). Fix the specific error the `path` locates, then re-validate. Do not freeform re-edit. Once `noodle validate` passes, run `noodle test`, then `noodle dev`.
39
11
 
40
12
  ## Error codes
41
13
 
@@ -5,10 +5,12 @@
5
5
  - Authenticate
6
6
  - Link and target
7
7
  - Deploy and inspect
8
+ - Connect into a host
8
9
  - Access modes
9
10
  - Org and members
10
11
  - Config and observability
11
12
  - Agent-safe CLI recipes
13
+ - Analytics
12
14
 
13
15
  ## Authenticate
14
16
 
@@ -22,6 +24,24 @@
22
24
 
23
25
  `noodle deploy` deploys the server. Then `noodle open` (latest URL), `noodle status`, `noodle inspect` (metadata, no secrets), `noodle smoke` (readiness diagnostics), and `noodle rollback <deploymentId>` to revert.
24
26
 
27
+ ## Connect into a host
28
+
29
+ Once deployed, register the server as a tool in a host with `noodle connect <host>` (`claude-code`, `codex`, `chatgpt`, `cursor`, `vscode`, `claude`, `inspector`) — it prints the exact config to paste.
30
+
31
+ - **Claude Code / Claude Desktop** (verified) — add the `mcpServers` block, or one-shot `claude mcp add-json noodle-server '<json>'`:
32
+
33
+ ```json
34
+ {
35
+ "mcpServers": {
36
+ "noodle-server": { "type": "https", "url": "https://<app>.mcp.noodleseed.dev" }
37
+ }
38
+ }
39
+ ```
40
+
41
+ - **Codex / Cursor / VS Code** — the same `mcpServers` block is emitted as a starting point (these hosts' config formats are not officially documented). Wiring a deployed Noodle server into Codex means registering that block in Codex's MCP config.
42
+ - **ChatGPT / Claude.ai** — no config file: open the host's Settings → Connectors → Add custom connector, paste the MCP URL, then authenticate.
43
+ - `noodle connect codex|claude-code --write` writes the project-local agent files (only these two targets).
44
+
25
45
  ## Access modes
26
46
 
27
47
  `noodle access set owner-only|org-members|authenticated|customers` controls who can call the deployed server. Hosted access is identity-based; never add static data-plane keys.
@@ -6,7 +6,7 @@ Flagship examples (one per capability). Read the matching example for the patter
6
6
  | :-- | :-- |
7
7
  | `hello` | Minimal TypeScript quickstart — a single tool, no connectors/widgets. |
8
8
  | `weather` | HTTP connectors, multi-step flows, and the sandboxed compute connector. |
9
- | `restaurant-pickup` | MCP Apps widgets, React `view` authoring, assets, branding, and handoff. |
9
+ | `food-ordering` | Consumer ordering MCP App widgets, app-only helpers, cart state, assets, branding, and handoff. |
10
10
  | `customer-auth` | End-user (customer) auth via OIDC/Firebase bridge with delegated credentials. |
11
11
  | `stateful-draft` | Durable, caller-scoped widget state handles with optimistic revisions. |
12
12
  | `perplexity` | A real SaaS API with bearer auth and a managed `secret`. |