@noodleseed/agent-kit 0.9.0 → 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.
- package/README.md +4 -3
- package/manifest.json +25 -15
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +14 -8
- package/skills/claude-code/references/agent-contract.md +43 -0
- package/skills/claude-code/references/authoring-workflow.md +43 -0
- package/skills/claude-code/references/compile-errors.md +1 -29
- package/skills/claude-code/references/deploy-and-ops.md +20 -0
- package/skills/claude-code/references/examples.md +1 -1
- package/skills/claude-code/references/sdk-surface.md +134 -9
- package/skills/claude-code/references/widgets-and-apps.md +175 -29
- package/skills/codex/SKILL.md +14 -8
- package/skills/codex/references/agent-contract.md +43 -0
- package/skills/codex/references/authoring-workflow.md +43 -0
- package/skills/codex/references/compile-errors.md +1 -29
- package/skills/codex/references/deploy-and-ops.md +20 -0
- package/skills/codex/references/examples.md +1 -1
- package/skills/codex/references/sdk-surface.md +134 -9
- package/skills/codex/references/widgets-and-apps.md +175 -29
|
@@ -3,43 +3,51 @@
|
|
|
3
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
4
|
Platform helper connectors are explicit subpath imports from `@noodleseed/one/platform` (`noodlePlatform`, `noodlePlatformCatalog`) when an app needs first-party hosted state APIs.
|
|
5
5
|
|
|
6
|
-
##
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- Exports by area
|
|
9
|
+
- Authoring signatures
|
|
10
|
+
- Recipes
|
|
11
|
+
|
|
12
|
+
## Exports by area
|
|
13
|
+
|
|
14
|
+
### Server & tools
|
|
7
15
|
|
|
8
16
|
- `server(name, options, definitions)` — the server/app root.
|
|
9
17
|
- `tool(name, { description, input, output, fulfil })` — a model-visible tool.
|
|
10
18
|
- `toolWithWidget(name, { ..., view })` — a model-visible tool that renders an MCP Apps widget.
|
|
11
19
|
- `toolForWidget(name, { ... })` — a widget-only helper tool, hidden from the model.
|
|
12
20
|
|
|
13
|
-
|
|
21
|
+
### Widgets & assets
|
|
14
22
|
|
|
15
23
|
- `widget(...)` — declare a widget/view component.
|
|
16
24
|
- `asset("./path")` — reference a packaged asset (e.g. an image).
|
|
17
25
|
- `annotations(...)` — tool/Apps annotation metadata.
|
|
18
26
|
|
|
19
|
-
|
|
27
|
+
### Connectors & flows
|
|
20
28
|
|
|
21
29
|
- `connector("id").version(...).http({...})` or `.compute(...)` — declarative data connectors.
|
|
22
30
|
- `when(...)` — declarative conditions for recorded flows (no native branching on runtime values).
|
|
23
31
|
|
|
24
|
-
|
|
32
|
+
### Resources & prompts
|
|
25
33
|
|
|
26
34
|
- `resource(name, { ... })` — an MCP resource.
|
|
27
35
|
- `prompt(name, { ... })` — an MCP prompt.
|
|
28
36
|
|
|
29
|
-
|
|
37
|
+
### Managed config
|
|
30
38
|
|
|
31
39
|
- `secret("NAME")` — reference a managed secret (operated via `noodle secrets`).
|
|
32
40
|
- `variable("NAME")` — reference a managed variable (operated via `noodle variables`).
|
|
33
41
|
|
|
34
|
-
|
|
42
|
+
### Customer auth
|
|
35
43
|
|
|
36
44
|
- `customerAuth.oidc(...)`, `.firebase(...)`, `.microsoft(...)`, or `.bridge(...)` — end-user/customer identity for `--access customers` deployments.
|
|
37
45
|
|
|
38
|
-
|
|
46
|
+
### Sessions
|
|
39
47
|
|
|
40
48
|
- `handoffSession(...)` — typed cross-host handoff session envelopes.
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
### Schemas
|
|
43
51
|
|
|
44
52
|
- `z` — Zod, for input/output schemas (compiles to JSON Schema 2020-12).
|
|
45
53
|
|
|
@@ -52,4 +60,121 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
|
|
|
52
60
|
- `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
|
|
53
61
|
- `widget(name, { title, view, csp?, domain?, permissions? })` declares reusable view metadata; `asset("./path")` packages local files.
|
|
54
62
|
- `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
|
|
55
|
-
- `state` defines durable widget state handles; `handoff` declares allowed external domains for safe host handoff.
|
|
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
|
+
});
|
|
156
|
+
|
|
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
|
+
```
|
|
@@ -3,60 +3,206 @@
|
|
|
3
3
|
## Contents
|
|
4
4
|
|
|
5
5
|
- Tools and views
|
|
6
|
-
- React authoring
|
|
7
6
|
- React hook surface
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
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,
|
|
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
26
|
| `useLayout` | Read host layout: `{ theme, displayMode, locale? }` (`theme` is `"light"`/`"dark"`, `displayMode` is `"inline"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
|
|
38
27
|
| `useOpenExternal` | Open an external link through the host (never `window.open`); the target origin must be listed in the server-level `handoff.allowedDomains`. |
|
|
39
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`.
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
```tsx
|
|
43
|
+
import type { ServerDefinition } from '@noodleseed/one';
|
|
44
|
+
import { generateHelpers } from '@noodleseed/one/react';
|
|
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
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 2. The tool declaration
|
|
42
95
|
|
|
43
|
-
|
|
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';
|
|
100
|
+
|
|
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}`;
|
|
104
|
+
|
|
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
|
+
```
|
|
44
159
|
|
|
45
|
-
|
|
160
|
+
## ChatGPT App = this widget + a domain
|
|
46
161
|
|
|
47
|
-
|
|
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:
|
|
48
163
|
|
|
49
|
-
|
|
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.
|
|
50
167
|
|
|
51
|
-
|
|
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.
|
|
52
169
|
|
|
53
|
-
|
|
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
|
+
```
|
|
54
200
|
|
|
55
|
-
|
|
201
|
+
`search` → `{ results: [{ id, title, url }] }`; `fetch` → `{ id, title, text, url, metadata? }`. `url` must be an absolute, user-openable https link for citation.
|
|
56
202
|
|
|
57
|
-
##
|
|
203
|
+
## Permissions and host bridge
|
|
58
204
|
|
|
59
|
-
|
|
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.
|
|
60
206
|
|
|
61
207
|
## Readiness and boundaries
|
|
62
208
|
|