@noodleseed/agent-kit 0.8.1 → 0.10.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.
@@ -1,39 +1,180 @@
1
1
  # @noodleseed/one SDK surface
2
2
 
3
- Import these from `@noodleseed/one`. They are declarative builders that emit manifest data — do not hand-author the manifest or runtime artifacts. React view helpers come from `@noodleseed/one/react` (`generateHelpers`).
3
+ Import these from `@noodleseed/one`. They are declarative builders that emit manifest data — do not hand-author the manifest or runtime artifacts. React view helpers come from `@noodleseed/one/react` (`generateHelpers`); the hook surface is documented in `widgets-and-apps.md`.
4
+ Platform helper connectors are explicit subpath imports from `@noodleseed/one/platform` (`noodlePlatform`, `noodlePlatformCatalog`) when an app needs first-party hosted state APIs.
4
5
 
5
- ## Server & tools
6
+ ## Contents
7
+
8
+ - Exports by area
9
+ - Authoring signatures
10
+ - Recipes
11
+
12
+ ## Exports by area
13
+
14
+ ### Server & tools
6
15
 
7
16
  - `server(name, options, definitions)` — the server/app root.
8
17
  - `tool(name, { description, input, output, fulfil })` — a model-visible tool.
9
18
  - `toolWithWidget(name, { ..., view })` — a model-visible tool that renders an MCP Apps widget.
10
19
  - `toolForWidget(name, { ... })` — a widget-only helper tool, hidden from the model.
11
20
 
12
- ## Widgets & assets
21
+ ### Widgets & assets
13
22
 
14
23
  - `widget(...)` — declare a widget/view component.
15
24
  - `asset("./path")` — reference a packaged asset (e.g. an image).
16
25
  - `annotations(...)` — tool/Apps annotation metadata.
17
26
 
18
- ## Connectors & flows
27
+ ### Connectors & flows
19
28
 
20
29
  - `connector("id").version(...).http({...})` or `.compute(...)` — declarative data connectors.
21
30
  - `when(...)` — declarative conditions for recorded flows (no native branching on runtime values).
22
31
 
23
- ## Resources & prompts
32
+ ### Resources & prompts
24
33
 
25
34
  - `resource(name, { ... })` — an MCP resource.
26
35
  - `prompt(name, { ... })` — an MCP prompt.
27
36
 
28
- ## Managed config
37
+ ### Managed config
29
38
 
30
39
  - `secret("NAME")` — reference a managed secret (operated via `noodle secrets`).
31
40
  - `variable("NAME")` — reference a managed variable (operated via `noodle variables`).
32
41
 
33
- ## Sessions
42
+ ### Customer auth
43
+
44
+ - `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments.
45
+
46
+ ### Sessions
34
47
 
35
48
  - `handoffSession(...)` — typed cross-host handoff session envelopes.
36
49
 
37
- ## Schemas
50
+ ### Schemas
51
+
52
+ - `z` — Zod, for input/output schemas (compiles to JSON Schema 2020-12).
53
+
54
+ ## Authoring signatures
55
+
56
+ - `server(name, options, definitions)` — `options` commonly includes `title`, `version`, `instructions`, `branding`, `auth`, `use`, `provides`, `state`, and `handoff`; `definitions` is the array of tools/resources/prompts/widgets.
57
+ - `tool(name, { description, input, output, annotations?, fulfil })` — `input`/`output` are Zod schemas; `fulfil({ input, connectors, user })` returns data matching `output`.
58
+ - `toolWithWidget(name, { description, input, output, fulfil, view })` — same as `tool`, plus `view: { component, entry }` for a React widget.
59
+ - `toolForWidget(name, { input, output, fulfil })` — helper tool for widget actions; hidden from the model.
60
+ - `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
61
+ - `widget(name, { title, view, csp?, domain?, permissions? })` declares reusable view metadata; `asset("./path")` packages local files.
62
+ - `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
63
+ - `state` defines durable widget state handles; `handoff` declares allowed external domains for safe host handoff.
64
+
65
+ ## Recipes
66
+
67
+ Minimal, complete, compiling recipes — author in `src/server.ts`, then `noodle validate`. Inside a `fulfil`, `ctx.input` (a prompt’s arguments or a templated resource’s URI variables) and `ctx.connectors` are **symbolic**: reference them to record a flow. Recording is not execution, so never branch on their runtime values with native `if` — use `when(...)`.
68
+
69
+ ### Resource
70
+
71
+ `resource(name, { uri, title?, description?, mimeType?, fulfil })`. `fulfil` returns `{ contents: [{ uri, mimeType, text }] }`. Use a fixed URI for a constant document, or a `{var}` template whose variable arrives on `ctx.input`.
72
+
73
+ ```ts
74
+ import { resource } from '@noodleseed/one';
75
+
76
+ // Fixed-URI resource: one constant document the model can read.
77
+ resource('changelog', {
78
+ uri: 'docs://changelog',
79
+ title: 'Changelog',
80
+ mimeType: 'text/markdown',
81
+ fulfil: () => ({
82
+ contents: [
83
+ { uri: 'docs://changelog', mimeType: 'text/markdown', text: 'Changelog: 1.0.0 first release' },
84
+ ],
85
+ }),
86
+ });
87
+
88
+ // {var} URI-template resource: the URI variable arrives on ctx.input (a symbolic ref).
89
+ resource('ticket', {
90
+ uri: 'tickets://{id}',
91
+ title: 'Support ticket',
92
+ mimeType: 'text/markdown',
93
+ fulfil: (ctx) => ({
94
+ contents: [
95
+ { uri: `tickets://${ctx.input.id}`, mimeType: 'text/markdown', text: `Ticket ${ctx.input.id}` },
96
+ ],
97
+ }),
98
+ });
99
+ ```
100
+
101
+ ### Prompt
102
+
103
+ `prompt(name, { title?, description?, arguments?, fulfil })`. `arguments` is a Zod object (each key becomes a `prompts/list` descriptor) or an explicit `[{ name, description?, required? }]` list. `fulfil` returns `{ messages: [{ role, content: { type: 'text', text } }] }`; supplied argument values arrive on `ctx.input`.
104
+
105
+ ```ts
106
+ import { prompt, z } from '@noodleseed/one';
107
+
108
+ prompt('summarize_ticket', {
109
+ title: 'Summarize ticket',
110
+ description: 'Draft a short summary of a support ticket.',
111
+ // A Zod object: each key becomes a prompts/list descriptor (or pass [{ name, description?, required? }]).
112
+ arguments: z.object({
113
+ ticket_id: z.string().describe('Ticket to summarize'),
114
+ tone: z.enum(['concise', 'detailed']).default('concise'),
115
+ }),
116
+ // Argument values arrive on ctx.input; return the prompts/get messages shape.
117
+ fulfil: (ctx) => ({
118
+ messages: [
119
+ {
120
+ role: 'user',
121
+ content: {
122
+ type: 'text',
123
+ text: `Summarize ticket ${ctx.input.ticket_id} in a ${ctx.input.tone} tone.`,
124
+ },
125
+ },
126
+ ],
127
+ }),
128
+ });
129
+ ```
130
+
131
+ ### Non-trivial tool: ctx connectors, annotations, visibility, async
132
+
133
+ `ctx` is `{ input, user, connectors }`. Bind connectors with `use` on the server, then call one inside `fulfil` to record a step. `annotations.readOnly()` / `annotations.action()` set the tool hints; `visibility` defaults to `['model', 'app']` — set `['app']` to hide a helper from the model. `fulfil` may be `async` (the compiler awaits it while recording).
134
+
135
+ ```ts
136
+ import { annotations, connector, server, tool, z } from '@noodleseed/one';
137
+
138
+ // A tool-facing HTTP connector, bound to the server via `use`, reachable as ctx.connectors.crm.
139
+ const crm = connector('crm')
140
+ .version('1.0.0')
141
+ .http({
142
+ baseUrl: 'https://crm.example.com',
143
+ allowedOrigins: ['https://crm.example.com'],
144
+ operations: {
145
+ get_ticket: {
146
+ type: 'read',
147
+ method: 'GET',
148
+ path: '/tickets',
149
+ query: ['id'],
150
+ input: { id: { type: 'string', required: true } },
151
+ output: { subject: { type: 'string' }, status: { type: 'string' } },
152
+ response: { subject: '${response.subject}', status: '${response.status}' },
153
+ },
154
+ },
155
+ });
38
156
 
39
- - `z` Zod, for input/output schemas (compiles to JSON Schema 2020-12).
157
+ export default server('support', { title: 'Support', version: '1.0.0', use: { crm } }, [
158
+ tool('get_ticket', {
159
+ description: 'Fetch a support ticket by id.',
160
+ input: z.object({ id: z.string() }),
161
+ output: z.object({ subject: z.string(), status: z.string() }),
162
+ annotations: annotations.readOnly(), // read-only hint for hosts
163
+ visibility: ['model', 'app'], // default; use ['app'] to hide the tool from the model
164
+ // ctx is { input, user, connectors }. A connector call records one flow step (a Ref) —
165
+ // recording is not execution, so never branch on the result with native if (use when).
166
+ fulfil: ({ input, connectors }) => {
167
+ const found = connectors.crm.get_ticket({ id: input.id });
168
+ return { subject: found.subject, status: found.status };
169
+ },
170
+ }),
171
+ tool('echo', {
172
+ description: 'Echo text back.',
173
+ input: z.object({ text: z.string() }),
174
+ output: z.object({ echo: z.string() }),
175
+ annotations: annotations.action(), // mutating / world-affecting hint
176
+ // fulfil may be async — the compiler awaits it while recording the flow.
177
+ fulfil: async ({ input }) => ({ echo: input.text }),
178
+ }),
179
+ ]);
180
+ ```
@@ -0,0 +1,39 @@
1
+ # Test in real hosts
2
+
3
+ Local `noodle dev` and `noodle devtools` prove the server works; the widget experience is only proven inside a real host. `noodle connect <client>` prints the exact setup flow per host.
4
+
5
+ ## Contents
6
+
7
+ - Local inspection first
8
+ - Agent hosts (Claude Code, Codex, editors)
9
+ - ChatGPT (developer mode)
10
+ - Claude
11
+ - Public URL for a local server
12
+ - What to verify
13
+
14
+ ## Local inspection first
15
+
16
+ Run `noodle dev` and inspect the loopback endpoint with MCP Inspector: `noodle connect inspector` prints the flow (`npx @modelcontextprotocol/inspector <printed endpoint>`). Preview widget metadata and rendering with `noodle devtools`.
17
+
18
+ ## Agent hosts (Claude Code, Codex, editors)
19
+
20
+ `noodle connect claude-code` / `noodle connect codex` (add `--write` for project-local setup). For other editors (`cursor`, `vscode`, `gemini`), `noodle connect <client>` prints the setup steps, and `noodle docs export --format llms` produces portable context. With a deployed endpoint, `noodle connect <client> --endpoint <url>` prints the MCP client registration config.
21
+
22
+ ## ChatGPT (developer mode)
23
+
24
+ 1. Deploy: `noodle deploy`, then `noodle open --print` for the hosted MCP URL (ChatGPT needs a public HTTPS endpoint, not loopback).
25
+ 2. In ChatGPT: Settings → Connectors → enable Developer mode → add the endpoint (`noodle connect chatgpt` prints these steps).
26
+ 3. Toggle the connector on in a new conversation and sign in when prompted; testers outside your org need a wider access mode (`noodle access set`).
27
+ 4. Test on mobile too — invoke the same connector from the ChatGPT iOS/Android apps to check widget layout.
28
+
29
+ ## Claude
30
+
31
+ `noodle connect claude` prints the flow: deploy, then add the hosted MCP URL as a custom connector in Claude settings and sign in when prompted. Widgets render in Apps-capable Claude surfaces; elsewhere the tool’s text/structured result is shown.
32
+
33
+ ## Public URL for a local server
34
+
35
+ To try an undeployed server in a host that requires a public URL, `noodle dev --tunnel` publishes a temporary public URL for the loopback endpoint (requires the external `cloudflared` binary on PATH). Treat it as a short-lived test URL — deploy for anything shared.
36
+
37
+ ## What to verify
38
+
39
+ Run a golden prompt set — direct (“use <tool> to…”), indirect (a natural request the model should route), and negative (requests that must not trigger the tool). Confirm the model picks the right tool with the right arguments, the widget renders and its actions work, external links open, and the experience degrades to readable text where Apps are unsupported. Symptoms → `references/troubleshooting.md`.
@@ -0,0 +1,29 @@
1
+ # Troubleshooting in hosts
2
+
3
+ ## Contents
4
+
5
+ - First moves
6
+ - Symptom map
7
+
8
+ ## First moves
9
+
10
+ Re-run the local gates before debugging in-host: `noodle validate`, `noodle check` (add `--target chatgpt` for ChatGPT-specific requirements), and `noodle doctor`. Confirm the CLI is current with `noodle update --check` and that the project-local skill is intact with `noodle agents doctor --json` — host metadata requirements evolve and fixes ship in the CLI/agent-kit. Never paste tokens, secrets, or `.env.noodle` values into prompts or logs while debugging.
11
+
12
+ For protocol/conformance checks, the headless harness is `@mcpjam/cli`, not a `noodle` subcommand. Use it against a local `noodle dev` URL without an access token, or against hosted URLs through the host/OAuth flow printed by `noodle connect`.
13
+
14
+ ## Symptom map
15
+
16
+ | Symptom | Likely cause | Fix |
17
+ | :-- | :-- | :-- |
18
+ | Images, fonts, or styles don’t load inside the widget | The host sandbox silently blocks origins not declared in the widget CSP | Add every asset origin to `csp: { resourceDomains: [...] }` (fetch/XHR origins go in `connectDomains`, embedded iframes in `frameDomains`), then re-run `noodle check --target chatgpt` |
19
+ | ChatGPT warns “Widget CSP is not set” | The widget declares no `csp` | Declare `csp` on the widget with the exact origins it uses |
20
+ | ChatGPT warns “Widget domain is not set” | No `domain` on the widget (required for app-store submission) | Set `domain: "https://…"` (one https origin per app) on each widget |
21
+ | External links do nothing, or show a safe-link warning | Link opened outside the host bridge, or the target origin is not allowlisted | Use `useOpenExternal()` (never `window.open`) and add the target origins to the server-level `handoff.allowedDomains` |
22
+ | Tool succeeds but no widget appears | The tool has no view, or the host surface doesn’t support MCP Apps | Use `toolWithWidget`, run `noodle check`, preview with `noodle devtools`; on non-Apps surfaces only the text/structured result renders |
23
+ | Widget shows stale or missing data | The widget reads `structuredContent`, which must match the `output` schema | Make `fulfil` return exactly the `output` shape (arrays and nested objects are supported); inspect the live result with `noodle devtools` |
24
+ | `useCallTool` fails from the widget | Tool name mismatch, or the helper tool is model-visible | List names with `noodle tools`; widget-only helpers must be declared with `toolForWidget` |
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
+ | 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
+ | 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
+ | Need to invoke a deployed tool from the terminal | The `noodle` CLI is not a general MCP client and has no `call` verb | Use `noodle test` for the local smoke, `noodle tools`/`resources`/`prompts` for local listing, MCP Inspector, or `npx @mcpjam/cli@latest tools call --url <url> ...` for headless MCP probing |
29
+ | 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` |
@@ -3,35 +3,206 @@
3
3
  ## Contents
4
4
 
5
5
  - Tools and views
6
- - React authoring
7
- - CSP and permissions
8
- - Host bridge
6
+ - React hook surface
7
+ - Worked widget recipe
8
+ - ChatGPT App = this widget + a domain
9
+ - Knowledge/source apps (search + fetch)
10
+ - Permissions and host bridge
9
11
  - Readiness and boundaries
10
12
 
11
13
  ## Tools and views
12
14
 
13
- 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" }`.
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.
14
16
 
15
- ## React authoring
17
+ ## React hook surface
16
18
 
17
- Author views as React components. Get typed helpers from `@noodleseed/one/react`:
19
+ Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
18
20
 
19
- ```ts
21
+ | Hook | Use for |
22
+ | :-- | :-- |
23
+ | `useToolInfo` | Read the invoking tool result; `structuredContent` is the widget’s typed data payload. |
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
+ | `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? }` (`theme` is `"light"`/`"dark"`, `displayMode` is `"inline"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
27
+ | `useOpenExternal` | Open an external link through the host (never `window.open`); the target origin must be listed in the server-level `handoff.allowedDomains`. |
28
+ | `useSendFollowUpMessage` | Send a follow-up prompt to the model from a user interaction: `send({ prompt })` — trigger only from an explicit user action. |
29
+ | `useAppFlow` | Manage named widget views with persisted params and back-stack state: `const flow = useAppFlow({ initialView, views })`. |
30
+ | `useHandoff` | Open server-created HTTP(S) handoff URLs through the host with status/error state; domain policy still comes from `handoff.allowedDomains`. |
31
+
32
+ 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>`).
33
+
34
+ ## Worked widget recipe
35
+
36
+ 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`).
37
+
38
+ ### 1. The view component
39
+
40
+ 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`.
41
+
42
+ ```tsx
43
+ import type { ServerDefinition } from '@noodleseed/one';
20
44
  import { generateHelpers } from '@noodleseed/one/react';
21
- const { useCallTool, useLayout, useToolInfo, useViewState } = generateHelpers<AppType>();
45
+
46
+ // One call wires the typed host bridge; destructure only the hooks this view uses.
47
+ const { useToolInfo, useCallTool, useViewState, useOpenExternal } =
48
+ generateHelpers<ServerDefinition>();
49
+
50
+ type OrderResult = {
51
+ readonly customer?: string;
52
+ readonly item?: string;
53
+ readonly total?: number;
54
+ readonly checkoutUrl?: string;
55
+ };
56
+
57
+ export default function OrderStatus() {
58
+ const shown = useToolInfo('show_order').structuredContent as OrderResult | undefined;
59
+ const placeOrder = useCallTool('place_order'); // calls the widget-only helper tool
60
+ const openExternal = useOpenExternal();
61
+ const [item, setItem] = useViewState('item', shown?.item ?? 'falafel_wrap'); // survives re-render
62
+ const confirmed = placeOrder.data?.structuredContent as { readonly status?: string } | undefined;
63
+ const total = shown?.total ?? 0;
64
+ const checkoutUrl = shown?.checkoutUrl ?? '';
65
+
66
+ return (
67
+ // data-llm mirrors the visible state back to the model as text context.
68
+ <main data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
69
+ <h1>Pickup order</h1>
70
+ <label>
71
+ Item
72
+ <select value={item} onChange={(event) => setItem(event.currentTarget.value)}>
73
+ <option value="falafel_wrap">Falafel Wrap</option>
74
+ <option value="lentil_soup">Lentil Soup</option>
75
+ <option value="mint_lemonade">Mint Lemonade</option>
76
+ </select>
77
+ </label>
78
+ <button
79
+ type="button"
80
+ disabled={placeOrder.isPending}
81
+ onClick={() => placeOrder.callTool({ customer: shown?.customer ?? 'Guest', item })}
82
+ >
83
+ {placeOrder.isPending ? 'Placing…' : 'Place order'}
84
+ </button>
85
+ <p>{confirmed?.status ?? `Total: $${total}`}</p>
86
+ <button type="button" onClick={() => openExternal(checkoutUrl)}>
87
+ Continue checkout
88
+ </button>
89
+ </main>
90
+ );
91
+ }
22
92
  ```
23
93
 
24
- 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>`).
94
+ ### 2. The tool declaration
95
+
96
+ `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.
97
+
98
+ ```ts
99
+ import { annotations, server, toolForWidget, toolWithWidget, z } from '@noodleseed/one';
25
100
 
26
- ## CSP, domain, and permissions
101
+ const item = z.enum(['falafel_wrap', 'lentil_soup', 'mint_lemonade']).default('falafel_wrap');
102
+ const checkoutUrl = (customer: string) =>
103
+ `https://orders.example.com/pickup?customer=${customer}`;
27
104
 
28
- 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.
105
+ export default server(
106
+ 'pickup',
107
+ {
108
+ title: 'Pickup',
109
+ version: '1.0.0',
110
+ // External-link targets the widget opens; the compiler derives ChatGPT redirect_domains from this.
111
+ handoff: { allowedDomains: ['https://orders.example.com'] },
112
+ },
113
+ [
114
+ toolWithWidget('show_order', {
115
+ description: 'Show the pickup order and render the ordering widget.',
116
+ // Declare tool annotations — a ChatGPT-submission requirement. A read → `readOnly()`.
117
+ annotations: annotations.readOnly(),
118
+ input: z.object({ customer: z.string().default('Guest') }),
119
+ output: z.object({
120
+ customer: z.string(),
121
+ item: z.string(),
122
+ total: z.number(),
123
+ checkoutUrl: z.string(),
124
+ }),
125
+ fulfil: ({ input }) => ({
126
+ customer: input.customer,
127
+ item: 'falafel_wrap',
128
+ total: 12,
129
+ checkoutUrl: checkoutUrl(input.customer),
130
+ }),
131
+ widgetTitle: 'Pickup order',
132
+ widgetDescription: 'Pick an item and place a pickup order.',
133
+ // A ChatGPT App is this widget + a domain: one https origin per app.
134
+ domain: 'https://pickup.example.com',
135
+ view: { component: 'order-status', entry: './views/order-status.tsx' },
136
+ csp: {
137
+ connectDomains: ['https://orders.example.com'],
138
+ resourceDomains: ['https://example.com'],
139
+ // Keep CSP origins exact and minimal. Add `frameDomains` ONLY if the widget embeds an
140
+ // iframe — it relaxes subframe rendering and triggers stricter ChatGPT review.
141
+ },
142
+ }),
143
+ // Widget-only helper the view calls with useCallTool('place_order'); hidden from the model.
144
+ toolForWidget('place_order', {
145
+ description: 'Place a pickup order from the widget.',
146
+ // A write that reaches the outside world → `action()` (not read-only, not destructive).
147
+ annotations: annotations.action(),
148
+ input: z.object({ customer: z.string().default('Guest'), item }),
149
+ output: z.object({ status: z.string(), item: z.string(), checkoutUrl: z.string() }),
150
+ fulfil: ({ input }) => ({
151
+ status: `Order placed for ${input.customer}.`,
152
+ item: input.item,
153
+ checkoutUrl: checkoutUrl(input.customer),
154
+ }),
155
+ }),
156
+ ],
157
+ );
158
+ ```
159
+
160
+ ## ChatGPT App = this widget + a domain
161
+
162
+ 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:
163
+
164
+ - `domain` on the widget — one https origin per app (required for app-store submission, optional for dev-mode testing).
165
+ - `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).
166
+ - server `handoff.allowedDomains` — the external-link targets `useOpenExternal()` opens.
167
+
168
+ 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.
169
+
170
+ 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.
171
+
172
+ **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.
173
+
174
+ ## Knowledge/source apps (ChatGPT): search + fetch
175
+
176
+ 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:
177
+
178
+ ```ts
179
+ import { annotations, server, tool, z } from '@noodleseed/one';
180
+
181
+ export default server('kb', { title: 'Knowledge base', version: '1.0.0' }, [
182
+ tool('search', {
183
+ description: 'Search the knowledge base; return citable results.',
184
+ annotations: annotations.readOnly(),
185
+ input: z.object({ query: z.string() }),
186
+ output: z.object({
187
+ results: z.array(z.object({ id: z.string(), title: z.string(), url: z.string() })),
188
+ }),
189
+ fulfil: ({ input }) => ({ results: [{ id: 'doc-1', title: `Match: ${input.query}`, url: 'https://example.com/doc-1' }] }),
190
+ }),
191
+ tool('fetch', {
192
+ description: 'Fetch one document by id for citation.',
193
+ annotations: annotations.readOnly(),
194
+ input: z.object({ id: z.string() }),
195
+ output: z.object({ id: z.string(), title: z.string(), text: z.string(), url: z.string() }),
196
+ fulfil: ({ input }) => ({ id: input.id, title: 'Doc', text: 'Full document text…', url: 'https://example.com/doc-1' }),
197
+ }),
198
+ ]);
199
+ ```
29
200
 
30
- 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.
201
+ `search` `{ results: [{ id, title, url }] }`; `fetch` `{ id, title, text, url, metadata? }`. `url` must be an absolute, user-openable https link for citation.
31
202
 
32
- ## Host bridge
203
+ ## Permissions and host bridge
33
204
 
34
- 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.
205
+ 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.
35
206
 
36
207
  ## Readiness and boundaries
37
208
 
@@ -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.8.1
5
- hash: f297c7e0fa62058d
4
+ version: 0.10.0
5
+ hash: 7cae1fbf5bdf6a0d
6
6
  ---
7
7
 
8
8
  # Noodle Seed
@@ -13,22 +13,32 @@ 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`).
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.
21
27
 
22
28
  ## References
23
29
 
24
30
  Load these on demand:
25
31
 
32
+ - `references/agent-contract.md` — the `--json` envelope, exit codes, and the three output modes.
26
33
  - `references/sdk-surface.md` — what to import from `@noodleseed/one` and which builder to use.
27
34
  - `references/cli-commands.md` — every `noodle` command, grouped by area.
28
35
  - `references/compile-errors.md` — fix `noodle validate` errors by code.
29
- - `references/authoring-workflow.md` — input paths (scrape / OpenAPI import / user interview), the validate→test→dev repair loop, connectors, and secrets/variables.
30
- - `references/widgets-and-apps.md` — MCP Apps, React `view` widgets, and CSP.
36
+ - `references/authoring-workflow.md` — input paths (scrape / OpenAPI import / user interview), the fit check, the validate→test→dev repair loop, connectors, and secrets/variables.
37
+ - `references/widgets-and-apps.md` — MCP Apps, React `view` widgets, the widget hook surface, output shaping, and CSP.
38
+ - `references/test-in-hosts.md` — connect and test in ChatGPT (developer mode), Claude, agent hosts, and MCP Inspector.
39
+ - `references/troubleshooting.md` — runtime symptom → cause → fix, in-host and hosted.
31
40
  - `references/deploy-and-ops.md` — login/link/deploy/status/access and hosted operations.
41
+ - `references/publishing.md` — submit to the ChatGPT apps directory and Claude connectors directory.
32
42
  - `references/examples.md` — flagship example index and a canonical `server.ts`.
33
43
 
34
44
  ## Safety
@@ -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`.