@noodleseed/agent-kit 0.90.0 → 0.92.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.
Files changed (102) hide show
  1. package/manifest.json +707 -275
  2. package/package.json +1 -1
  3. package/skills/claude-code/SKILL.md +1 -1
  4. package/skills/claude-code/authoring-mcp-servers/SKILL.md +4 -4
  5. package/skills/claude-code/authoring-mcp-servers/references/authoring-workflow.md +451 -0
  6. package/skills/claude-code/authoring-mcp-servers/references/build-an-mcp-server.md +56 -0
  7. package/skills/claude-code/authoring-mcp-servers/references/sdk-surface.md +258 -0
  8. package/skills/claude-code/building-mcp-apps/SKILL.md +4 -4
  9. package/skills/claude-code/building-mcp-apps/references/build-an-mcp-app.md +53 -0
  10. package/skills/claude-code/building-mcp-apps/references/experience-design.md +155 -0
  11. package/skills/claude-code/building-mcp-apps/references/widgets-and-apps.md +277 -0
  12. package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +3 -3
  13. package/skills/claude-code/connecting-apis-to-mcp/references/authoring-workflow.md +451 -0
  14. package/skills/claude-code/connecting-apis-to-mcp/references/connect-an-api.md +245 -0
  15. package/skills/claude-code/creating-product-agent-guides/SKILL.md +2 -2
  16. package/skills/claude-code/creating-product-agent-guides/references/product-agent-guides.md +55 -0
  17. package/skills/claude-code/debugging-mcp-delivery/SKILL.md +4 -4
  18. package/skills/claude-code/debugging-mcp-delivery/references/inspect-hosted.md +26 -0
  19. package/skills/claude-code/debugging-mcp-delivery/references/troubleshooting.md +39 -0
  20. package/skills/claude-code/debugging-mcp-delivery/references/verify-and-recover.md +67 -0
  21. package/skills/claude-code/deploying-mcp-services/SKILL.md +3 -3
  22. package/skills/claude-code/deploying-mcp-services/references/cli-commands.md +106 -0
  23. package/skills/claude-code/deploying-mcp-services/references/deploy-and-ops.md +25 -0
  24. package/skills/claude-code/designing-mcp-products/SKILL.md +3 -3
  25. package/skills/claude-code/designing-mcp-products/references/authoring-workflow.md +451 -0
  26. package/skills/claude-code/designing-mcp-products/references/experience-design.md +155 -0
  27. package/skills/claude-code/embedding-mcp-assistants/SKILL.md +11 -10
  28. package/skills/claude-code/embedding-mcp-assistants/references/authoring-workflow.md +451 -0
  29. package/skills/claude-code/embedding-mcp-assistants/references/embedded-assistant.md +876 -0
  30. package/skills/claude-code/examples/acme-discovery/README.md +7 -5
  31. package/skills/claude-code/examples/customer-auth/README.md +200 -52
  32. package/skills/claude-code/examples/customer-auth/src/server.ts +3 -0
  33. package/skills/claude-code/examples/customer-auth/test/server.test.ts +6 -0
  34. package/skills/claude-code/examples/customer-auth/vitest.config.ts +6 -0
  35. package/skills/claude-code/examples/food-ordering/README.md +1 -1
  36. package/skills/claude-code/examples/food-ordering/src/server.ts +1 -0
  37. package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
  38. package/skills/claude-code/publishing-mcp-integrations/SKILL.md +3 -3
  39. package/skills/claude-code/publishing-mcp-integrations/references/app-directory-compliance.md +59 -0
  40. package/skills/claude-code/publishing-mcp-integrations/references/publishing.md +63 -0
  41. package/skills/claude-code/references/authoring-workflow.md +8 -0
  42. package/skills/claude-code/references/embedded-assistant.md +259 -67
  43. package/skills/claude-code/references/sdk-surface.md +1 -1
  44. package/skills/claude-code/reporting-noodle-feedback/SKILL.md +2 -2
  45. package/skills/claude-code/reporting-noodle-feedback/references/feedback.md +80 -0
  46. package/skills/claude-code/verifying-mcp-delivery/SKILL.md +3 -3
  47. package/skills/claude-code/verifying-mcp-delivery/references/test-in-hosts.md +39 -0
  48. package/skills/claude-code/verifying-mcp-delivery/references/verify-and-recover.md +67 -0
  49. package/skills/claude-code/wrapping-existing-applications/SKILL.md +4 -4
  50. package/skills/claude-code/wrapping-existing-applications/references/authoring-workflow.md +451 -0
  51. package/skills/claude-code/wrapping-existing-applications/references/tool-design.md +107 -0
  52. package/skills/claude-code/wrapping-existing-applications/references/wrap-existing-app.md +65 -0
  53. package/skills/codex/SKILL.md +1 -1
  54. package/skills/codex/authoring-mcp-servers/SKILL.md +4 -4
  55. package/skills/codex/authoring-mcp-servers/references/authoring-workflow.md +451 -0
  56. package/skills/codex/authoring-mcp-servers/references/build-an-mcp-server.md +56 -0
  57. package/skills/codex/authoring-mcp-servers/references/sdk-surface.md +258 -0
  58. package/skills/codex/building-mcp-apps/SKILL.md +4 -4
  59. package/skills/codex/building-mcp-apps/references/build-an-mcp-app.md +53 -0
  60. package/skills/codex/building-mcp-apps/references/experience-design.md +155 -0
  61. package/skills/codex/building-mcp-apps/references/widgets-and-apps.md +277 -0
  62. package/skills/codex/connecting-apis-to-mcp/SKILL.md +3 -3
  63. package/skills/codex/connecting-apis-to-mcp/references/authoring-workflow.md +451 -0
  64. package/skills/codex/connecting-apis-to-mcp/references/connect-an-api.md +245 -0
  65. package/skills/codex/creating-product-agent-guides/SKILL.md +2 -2
  66. package/skills/codex/creating-product-agent-guides/references/product-agent-guides.md +55 -0
  67. package/skills/codex/debugging-mcp-delivery/SKILL.md +4 -4
  68. package/skills/codex/debugging-mcp-delivery/references/inspect-hosted.md +26 -0
  69. package/skills/codex/debugging-mcp-delivery/references/troubleshooting.md +39 -0
  70. package/skills/codex/debugging-mcp-delivery/references/verify-and-recover.md +67 -0
  71. package/skills/codex/deploying-mcp-services/SKILL.md +3 -3
  72. package/skills/codex/deploying-mcp-services/references/cli-commands.md +106 -0
  73. package/skills/codex/deploying-mcp-services/references/deploy-and-ops.md +25 -0
  74. package/skills/codex/designing-mcp-products/SKILL.md +3 -3
  75. package/skills/codex/designing-mcp-products/references/authoring-workflow.md +451 -0
  76. package/skills/codex/designing-mcp-products/references/experience-design.md +155 -0
  77. package/skills/codex/embedding-mcp-assistants/SKILL.md +11 -10
  78. package/skills/codex/embedding-mcp-assistants/references/authoring-workflow.md +451 -0
  79. package/skills/codex/embedding-mcp-assistants/references/embedded-assistant.md +876 -0
  80. package/skills/codex/examples/acme-discovery/README.md +7 -5
  81. package/skills/codex/examples/customer-auth/README.md +200 -52
  82. package/skills/codex/examples/customer-auth/src/server.ts +3 -0
  83. package/skills/codex/examples/customer-auth/test/server.test.ts +6 -0
  84. package/skills/codex/examples/customer-auth/vitest.config.ts +6 -0
  85. package/skills/codex/examples/food-ordering/README.md +1 -1
  86. package/skills/codex/examples/food-ordering/src/server.ts +1 -0
  87. package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
  88. package/skills/codex/publishing-mcp-integrations/SKILL.md +3 -3
  89. package/skills/codex/publishing-mcp-integrations/references/app-directory-compliance.md +59 -0
  90. package/skills/codex/publishing-mcp-integrations/references/publishing.md +63 -0
  91. package/skills/codex/references/authoring-workflow.md +8 -0
  92. package/skills/codex/references/embedded-assistant.md +259 -67
  93. package/skills/codex/references/sdk-surface.md +1 -1
  94. package/skills/codex/reporting-noodle-feedback/SKILL.md +2 -2
  95. package/skills/codex/reporting-noodle-feedback/references/feedback.md +80 -0
  96. package/skills/codex/verifying-mcp-delivery/SKILL.md +3 -3
  97. package/skills/codex/verifying-mcp-delivery/references/test-in-hosts.md +39 -0
  98. package/skills/codex/verifying-mcp-delivery/references/verify-and-recover.md +67 -0
  99. package/skills/codex/wrapping-existing-applications/SKILL.md +4 -4
  100. package/skills/codex/wrapping-existing-applications/references/authoring-workflow.md +451 -0
  101. package/skills/codex/wrapping-existing-applications/references/tool-design.md +107 -0
  102. package/skills/codex/wrapping-existing-applications/references/wrap-existing-app.md +65 -0
@@ -0,0 +1,277 @@
1
+ # MCP Apps and widgets
2
+
3
+ ## Contents
4
+
5
+ - Tools and views
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
11
+ - Readiness and boundaries
12
+
13
+ ## Tools and views
14
+
15
+ Use `tool(name, { description, input, output, fulfil, view })` for a model-visible tool that renders a widget, and the same `tool(name, { ..., visibility: ["app"] })` for an app-only helper hidden from the model. When a view is valid only for narrow explicit intent, add `modelVisibility: { latestMessageIncludesAny: ["open the form", ...] }`; Noodle normalizes and matches those literal phrases against the latest user message before model discovery and fails closed on malformed data. Add `oncePerSession: true` when a successful model-selected view must not repeat in that conversation, and `requiredWhenVisible: true` only when a matching turn must render that sole required tool before normal model discovery resumes. These options control presentation and relevance, never idempotency or authorization. A `view` is `{ component: "name", entry: "./views/name.tsx" }` — a React component the compiler bundles at validate/deploy time.
16
+
17
+ ## Noodle Design default
18
+
19
+ Generated widgets, official examples, and agent-authored MCP Apps must start with `@noodleseed/one/react` primitives and semantic tokens. Custom React/CSS or third-party components remain valid when the kit lacks the required behavior or the developer explicitly requests them.
20
+
21
+ `noodle init my-app` defaults to the SaaS profile: federated OIDC placeholders, one explicit context-provider tool, MCP App UI, resource, prompt, state contract, branding, handoff, and embedded assistant. Begin by replacing the IdP/audience/domain placeholders. Use `--template widget`, `hello`, or `http-api` only when that narrower profile is intentional.
22
+
23
+ The default composition rule is: build the smallest useful conversational surface. Inline has one purpose, one primary action, and at most two visible actions. Use progressive disclosure or a later conversational turn for secondary detail; request fullscreen only when the user asks or the task genuinely needs it. Never use nested scrolling. At 280px and wider, the widget must remain one-column, readable, touch-safe, and free of horizontal overflow. Remove secondary chrome before shrinking essential content.
24
+
25
+ Treat 1 MiB of uncompressed UTF-8 HTML as the recommended initial React-widget budget, not a compatibility wall. Noodle Seed accepts up to 10 MiB per compiled widget and 20 MiB across a deployment; `noodle check` reports raw and gzip-estimated sizes. Keep large media and dynamic datasets in hosted assets/resources or bounded app-only tool responses instead of inflating the initial widget resource.
26
+
27
+ Every production widget handles loading, empty, partial, stale, error, retry, and success states. Prefill fields from known tool results and choose safe, reversible defaults; preserve the user’s work across rerenders, and never preselect a consequential action. Use public React primitives and branding tokens. Never author against `ns-*`, `nsr-*`, or example-local `--nw-*` classes/tokens; those are implementation details, not alternate design systems.
28
+
29
+ Keep the complete `useToolInfo()` result. An empty result envelope is pending, `isError` is an explicit tool error, and any non-pending success must validate every required field and identifier before it becomes UI data. Render a malformed-result error when validation fails, and render dependent actions only after validation succeeds. Safe local input defaults may follow valid hydration; they must never fabricate a loaded business record.
30
+
31
+ ## React hook surface
32
+
33
+ Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
34
+
35
+ | Hook | Use for |
36
+ | :-- | :-- |
37
+ | `useWidgetReady` | Report when the standard MCP Apps bridge has connected; keep tool-backed controls disabled (or render loading) until this returns `true`. |
38
+ | `useToolInfo` | Read the complete invoking tool result: treat `{}` as pending, handle `isError`, validate every required `structuredContent` field and identifier, reject malformed success data, and render dependent actions only after validation succeeds. |
39
+ | `useCallTool` | Call a tool from the widget — returns `{ status, callTool, callToolAsync, data, structuredContent, error, reset }`; target a model-visible tool or a hidden `tool` helper. |
40
+ | `useViewState` | Persist per-widget UI state across re-renders and restores: `const [value, setValue] = useViewState("key", initial)`. |
41
+ | `useLayout` | Read host layout: `{ theme, displayMode, locale?, host?, supports? }` (`displayMode` is `"inline"`/`"pip"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
42
+ | `useBranding` | Read the server-level brand kit (`name`, `accent`, `surface`, `radius`, themed logo/mark/avatar URLs). Nothing is applied for you: widget CSS is yours, so map the values you need onto your own custom properties (e.g. `style={{ "--my-accent": useBranding().accent }}`) instead of hard-coding the brand color a second time. |
43
+ | `useRequestDisplayMode` | Request a host-mediated layout change such as fullscreen; treat it as best-effort and keep inline rendering useful. |
44
+ | `useOpenExternal` | Open an external link through the host (never `window.open`); the target origin must be listed in the server-level `handoff.allowedDomains`. |
45
+ | `useSendFollowUpMessage` | Send a follow-up prompt to the model from a user interaction: `send({ prompt })` — trigger only from an explicit user action. |
46
+ | `useUpdateModelContext` | Publish one compact, cohesive author-selected text/structured snapshot through the standard MCP Apps model-context channel; each call replaces the prior snapshot, so include every still-relevant field and check `useLayout().supports?.modelContext` first. |
47
+ | `useWidgetLifecycle` | Calling the hook auto-publishes `mounted` and listens for host `cancelled`/`dismissed`; use its publisher for author-owned `submitted` or app milestones, include a complete safe replacement snapshot, and pair explicit submit/cancel with `useSendFollowUpMessage` when an immediate reply is wanted. |
48
+ | `useAppFlow` | Manage named widget views with persisted params and back-stack state: `const flow = useAppFlow({ initialView, views })`. |
49
+ | `useHandoff` | Open server-created HTTP(S) handoff URLs through the host with status/error state; domain policy still comes from `handoff.allowedDomains`. |
50
+
51
+ Bind interactive elements to tools (`useCallTool("place_order")`), keep bridge-backed controls disabled until `useWidgetReady()` is true, drive named views with `useAppFlow(...)`, open server-created handoffs with `useHandoff()`, and publish one compact, safe, cohesive snapshot with `useUpdateModelContext()` when `useLayout().supports?.modelContext` is true. Generated form workflows use the portable `<Form onSubmit={() => void submit()}>` component with a submit button; never use an intrinsic React `<form>` or add browser-navigation `action`, `method`, or `target` attributes. A sandbox can block native activation before React receives `onSubmit`, so `preventDefault()` inside an intrinsic handler is not a portability fix. Standalone actions use an explicit `type="button"` and call the tool from their click handler. In every case the widget calls the standard MCP Apps tool bridge itself — never rely on a host to translate native form submission into a tool call, and never branch on a host name. Every model-context or lifecycle publication replaces the prior snapshot rather than merging fields, so include everything the model should still know. Calling `useWidgetLifecycle("name")` automatically publishes `mounted`, listens for host `cancelled` and `dismissed`, and returns a publisher for author-owned `submitted` or app-specific milestones; `mounted` is not proof that the host presented pixels. Both hooks use the standard MCP Apps model-context channel, not a host-specific API. These updates affect future model context but do not start a model turn. When an explicit user submit/cancel should receive an immediate reply, also call `useSendFollowUpMessage()` from that user action. `data-llm` may remain a DOM inspection hint, but it is not the bidirectional model-state contract. 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`, `Form`, `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>`); use a `data-action` button with `type="button"` instead of native form submission.
52
+
53
+ ## Worked widget recipe
54
+
55
+ 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`).
56
+
57
+ ### 1. The view component
58
+
59
+ 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 publish cohesive model-context snapshots plus author-owned lifecycle milestones.
60
+
61
+ ```tsx
62
+ import { useEffect } from 'react';
63
+ import type { ServerDefinition } from '@noodleseed/one';
64
+ import { Action, ActionBar, AsyncBoundary, Feedback, Field, Flow, Frame, Region, Select, generateHelpers } from '@noodleseed/one/react';
65
+
66
+ // One call wires the typed host bridge; destructure only the hooks this view uses.
67
+ const { useToolInfo, useCallTool, useViewState, useLayout, useOpenExternal, useSendFollowUpMessage, useUpdateModelContext, useWidgetLifecycle, useWidgetReady } =
68
+ generateHelpers<ServerDefinition>();
69
+
70
+ type OrderResult = {
71
+ readonly customer: string;
72
+ readonly item: string;
73
+ readonly total: number;
74
+ readonly checkoutUrl: string;
75
+ };
76
+
77
+ function isOrderResult(value: unknown): value is OrderResult {
78
+ if (value === null || typeof value !== 'object') return false;
79
+ const result = value as Partial<OrderResult>;
80
+ return (
81
+ typeof result.customer === 'string' && result.customer.length > 0 &&
82
+ typeof result.item === 'string' && result.item.length > 0 &&
83
+ typeof result.total === 'number' && Number.isFinite(result.total) &&
84
+ typeof result.checkoutUrl === 'string' && result.checkoutUrl.startsWith('https://')
85
+ );
86
+ }
87
+
88
+ export default function OrderStatus() {
89
+ const ready = useWidgetReady();
90
+ const toolInfo = useToolInfo('show_order');
91
+ const isPending = !ready || Object.keys(toolInfo).length === 0;
92
+ const shown = isOrderResult(toolInfo.structuredContent) ? toolInfo.structuredContent : undefined;
93
+ const placeOrder = useCallTool('place_order'); // calls the widget-only helper tool
94
+ const { supports } = useLayout();
95
+ const openExternal = useOpenExternal();
96
+ const sendFollowUpMessage = useSendFollowUpMessage();
97
+ const updateModelContext = useUpdateModelContext();
98
+ const publishLifecycle = useWidgetLifecycle('pickup-order');
99
+ const [item, setItem] = useViewState('item', shown?.item ?? 'falafel_wrap'); // survives re-render
100
+ const confirmed = placeOrder.data?.structuredContent as { readonly status?: string } | undefined;
101
+ const total = shown?.total ?? 0;
102
+ const checkoutUrl = shown?.checkoutUrl ?? '';
103
+
104
+ useEffect(() => {
105
+ if (!shown || !supports?.modelContext) return;
106
+ void updateModelContext({
107
+ content: [{ type: 'text', text: `Pickup order: ${item}, total ${total}` }],
108
+ structuredContent: { widget: { name: 'pickup-order', lifecycle: 'active' }, order: { item, total } },
109
+ });
110
+ }, [item, shown, supports?.modelContext, total, updateModelContext]);
111
+
112
+ async function submitOrder() {
113
+ if (!shown) return;
114
+ await placeOrder.callTool({ customer: shown.customer, item });
115
+ if (supports?.modelContext) {
116
+ await publishLifecycle('submitted', { surface: 'order', item, total, status: 'submitted' });
117
+ }
118
+ if (supports?.followUpMessage) {
119
+ await sendFollowUpMessage({ prompt: `The pickup order was submitted for ${item}. Confirm the result.` });
120
+ }
121
+ }
122
+
123
+ if (isPending) return <Feedback status="loading">Loading the order…</Feedback>;
124
+ if (toolInfo.isError) return <Feedback status="error">Could not load the order.</Feedback>;
125
+ if (!shown) return <Feedback status="error">The order result was incomplete.</Feedback>;
126
+
127
+ return (
128
+ // data-llm is an inspection hint; useUpdateModelContext above is the model-visible channel.
129
+ <Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown.customer}: ${item}, total ${total}`}>
130
+ <Flow variant="stack">
131
+ <AsyncBoundary state={placeOrder} loading="Placing order…" error={(error) => error.message}>
132
+ <Region title="Order" description="Choose one item for pickup.">
133
+ <Field label="Item">
134
+ <Select value={item} onChange={(event) => setItem(event.currentTarget.value)} options={[
135
+ { value: 'falafel_wrap', label: 'Falafel Wrap' },
136
+ { value: 'lentil_soup', label: 'Lentil Soup' },
137
+ { value: 'mint_lemonade', label: 'Mint Lemonade' },
138
+ ]} />
139
+ </Field>
140
+ </Region>
141
+ {confirmed?.status ? <Feedback status="success">{confirmed.status}</Feedback> : null}
142
+ <ActionBar>
143
+ <Action type="button" variant="primary" disabled={!ready} pending={placeOrder.isPending} pendingLabel="Placing…"
144
+ onClick={submitOrder}
145
+ >
146
+ Place order
147
+ </Action>
148
+ <Action onClick={() => openExternal(checkoutUrl)}>
149
+ Continue checkout
150
+ </Action>
151
+ </ActionBar>
152
+ </AsyncBoundary>
153
+ </Flow>
154
+ </Frame>
155
+ );
156
+ }
157
+ ```
158
+
159
+ ### 2. The tool declaration
160
+
161
+ `tool` declares both the model-visible tool that renders the view and app-only helpers the view calls. Put `visibility: ["app"]` on each helper. 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.
162
+
163
+ ```ts
164
+ import { annotations, server, tool, z } from '@noodleseed/one';
165
+
166
+ const item = z.enum(['falafel_wrap', 'lentil_soup', 'mint_lemonade']).default('falafel_wrap');
167
+ const checkoutUrl = (customer: string) =>
168
+ `https://orders.example.com/pickup?customer=${customer}`;
169
+
170
+ export default server(
171
+ 'pickup',
172
+ {
173
+ title: 'Pickup',
174
+ version: '1.0.0',
175
+ // External-link targets the widget opens; the compiler derives ChatGPT redirect_domains from this.
176
+ handoff: { allowedDomains: ['https://orders.example.com'] },
177
+ },
178
+ [
179
+ tool('show_order', {
180
+ description: 'Show the pickup order and render the ordering widget.',
181
+ // Declare tool annotations — a ChatGPT-submission requirement. A read → `readOnly()`.
182
+ annotations: annotations.readOnly(),
183
+ input: z.object({ customer: z.string().default('Guest') }),
184
+ output: z.object({
185
+ customer: z.string(),
186
+ item: z.string(),
187
+ total: z.number(),
188
+ checkoutUrl: z.string(),
189
+ }),
190
+ fulfil: ({ input }) => ({
191
+ customer: input.customer,
192
+ item: 'falafel_wrap',
193
+ total: 12,
194
+ checkoutUrl: checkoutUrl(input.customer),
195
+ }),
196
+ viewTitle: 'Pickup order',
197
+ viewDescription: 'Pick an item and place a pickup order.',
198
+ // A ChatGPT App is this widget + a domain: one https origin per app.
199
+ domain: 'https://pickup.example.com',
200
+ view: { component: 'order-status', entry: './views/order-status.tsx' },
201
+ csp: {
202
+ connectDomains: ['https://orders.example.com'],
203
+ resourceDomains: ['https://example.com'],
204
+ // Keep CSP origins exact and minimal. Add `frameDomains` ONLY if the widget embeds an
205
+ // iframe — it relaxes subframe rendering and triggers stricter ChatGPT review.
206
+ },
207
+ }),
208
+ // Widget-only helper the view calls with useCallTool('place_order'); hidden from the model.
209
+ tool('place_order', {
210
+ visibility: ['app'],
211
+ description: 'Place a pickup order from the widget.',
212
+ // Widget-mediated direct action: explicit false is equivalent to omitting confirmation.
213
+ annotations: annotations.action({ confirm: false }),
214
+ input: z.object({ customer: z.string().default('Guest'), item }),
215
+ output: z.object({ status: z.string(), item: z.string(), checkoutUrl: z.string() }),
216
+ fulfil: ({ input }) => ({
217
+ status: `Order placed for ${input.customer}.`,
218
+ item: input.item,
219
+ checkoutUrl: checkoutUrl(input.customer),
220
+ }),
221
+ }),
222
+ ],
223
+ );
224
+ ```
225
+
226
+ ## ChatGPT App = this widget + a domain
227
+
228
+ 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:
229
+
230
+ - `domain` on the widget — one https origin per app (required for app-store submission, optional for dev-mode testing).
231
+ - `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). These three lists are the complete CSP surface — there is no `base-uri` list.
232
+ - server `handoff.allowedDomains` — the external-link targets `useOpenExternal()` opens.
233
+
234
+ 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.
235
+
236
+ 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.
237
+
238
+ **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.
239
+
240
+ ## Knowledge/source apps (ChatGPT): search + fetch
241
+
242
+ 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:
243
+
244
+ ```ts
245
+ import { annotations, server, tool, z } from '@noodleseed/one';
246
+
247
+ export default server('kb', { title: 'Knowledge base', version: '1.0.0' }, [
248
+ tool('search', {
249
+ description: 'Search the knowledge base; return citable results.',
250
+ annotations: annotations.readOnly(),
251
+ input: z.object({ query: z.string() }),
252
+ output: z.object({
253
+ results: z.array(z.object({ id: z.string(), title: z.string(), url: z.string() })),
254
+ }),
255
+ fulfil: ({ input }) => ({ results: [{ id: 'doc-1', title: `Match: ${input.query}`, url: 'https://example.com/doc-1' }] }),
256
+ }),
257
+ tool('fetch', {
258
+ description: 'Fetch one document by id for citation.',
259
+ annotations: annotations.readOnly(),
260
+ input: z.object({ id: z.string() }),
261
+ output: z.object({ id: z.string(), title: z.string(), text: z.string(), url: z.string() }),
262
+ fulfil: ({ input }) => ({ id: input.id, title: 'Doc', text: 'Full document text…', url: 'https://example.com/doc-1' }),
263
+ }),
264
+ ]);
265
+ ```
266
+
267
+ `search` → `{ results: [{ id, title, url }] }`; `fetch` → `{ id, title, text, url, metadata? }`. `url` must be an absolute, user-openable https link for citation.
268
+
269
+ ## Permissions and host bridge
270
+
271
+ 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.
272
+
273
+ ## Readiness and boundaries
274
+
275
+ React views use the app-local Vite bundler. When adding a view to an existing project that does not declare Vite, run `npm install --save-dev vite` before validation.
276
+
277
+ Run `noodle validate` first, `noodle check` for widget/Apps readiness, and `noodle devtools` to preview metadata and rendering. Brand via the `server` `branding` tokens (the compiler derives the palette); do not inject raw global CSS.
@@ -3,7 +3,7 @@ name: connecting-apis-to-mcp
3
3
  description: "Use when all four API-evidence inputs exist—and only then: API base URL, authentication scheme, representative safe read, and observed response."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.90.0 hash:21bbd3ec441ffd30 -->
6
+ <!-- noodle-skill version:0.92.0 hash:8020811f1769c538 -->
7
7
 
8
8
  # connecting-apis-to-mcp
9
9
 
@@ -26,8 +26,8 @@ Connect a real API using managed credentials and mappings proven against observe
26
26
 
27
27
  ## Workflow
28
28
 
29
- Read and follow the canonical playbook `references/connect-an-api.md` at `../noodle-seed/references/connect-an-api.md`. It owns the workflow; do not recreate it here or load the command catalog speculatively.
30
- Load `references/authoring-workflow.md` at `../noodle-seed/references/authoring-workflow.md` only when the playbook or observed evidence names that concern.
29
+ Read and follow the canonical playbook at `references/connect-an-api.md`. It owns the workflow; do not recreate it here or load the command catalog speculatively.
30
+ Load the supporting reference at `references/authoring-workflow.md` only when the playbook or observed evidence names that concern.
31
31
 
32
32
  ## Verification evidence
33
33