@noodleseed/agent-kit 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +49 -49
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +1 -1
- package/skills/claude-code/examples/acme-bistro/README.md +1 -1
- package/skills/claude-code/examples/acme-bistro/design/UX-Document.md +4 -4
- package/skills/claude-code/examples/acme-bistro/design/api-contract.md +3 -3
- package/skills/claude-code/examples/acme-bistro/design/wireframe.html +3 -3
- package/skills/claude-code/examples/acme-bistro/src/server.ts +8 -6
- package/skills/claude-code/examples/acme-discovery/README.md +1 -1
- package/skills/claude-code/examples/acme-discovery/design/UX-Document.md +3 -3
- package/skills/claude-code/examples/acme-discovery/design/wireframe.html +2 -2
- package/skills/claude-code/examples/acme-discovery/src/server.ts +6 -5
- package/skills/claude-code/examples/acme-tasks/README.md +30 -1
- package/skills/claude-code/examples/acme-tasks/design/UX-Document.md +2 -2
- package/skills/claude-code/examples/acme-tasks/design/wireframe.html +6 -6
- package/skills/claude-code/examples/acme-tasks/src/server.ts +6 -5
- package/skills/claude-code/examples/food-ordering/src/server.ts +46 -56
- package/skills/claude-code/examples/weather/src/server.ts +29 -41
- package/skills/claude-code/references/authoring-workflow.md +15 -10
- package/skills/claude-code/references/cli-commands.md +1 -0
- package/skills/claude-code/references/compile-errors.md +2 -2
- package/skills/claude-code/references/connect-an-api.md +2 -2
- package/skills/claude-code/references/experience-design.md +1 -1
- package/skills/claude-code/references/sdk-surface.md +16 -16
- package/skills/claude-code/references/troubleshooting.md +2 -2
- package/skills/claude-code/references/widgets-and-apps.md +9 -8
- package/skills/codex/SKILL.md +1 -1
- package/skills/codex/examples/acme-bistro/README.md +1 -1
- package/skills/codex/examples/acme-bistro/design/UX-Document.md +4 -4
- package/skills/codex/examples/acme-bistro/design/api-contract.md +3 -3
- package/skills/codex/examples/acme-bistro/design/wireframe.html +3 -3
- package/skills/codex/examples/acme-bistro/src/server.ts +8 -6
- package/skills/codex/examples/acme-discovery/README.md +1 -1
- package/skills/codex/examples/acme-discovery/design/UX-Document.md +3 -3
- package/skills/codex/examples/acme-discovery/design/wireframe.html +2 -2
- package/skills/codex/examples/acme-discovery/src/server.ts +6 -5
- package/skills/codex/examples/acme-tasks/README.md +30 -1
- package/skills/codex/examples/acme-tasks/design/UX-Document.md +2 -2
- package/skills/codex/examples/acme-tasks/design/wireframe.html +6 -6
- package/skills/codex/examples/acme-tasks/src/server.ts +6 -5
- package/skills/codex/examples/food-ordering/src/server.ts +46 -56
- package/skills/codex/examples/weather/src/server.ts +29 -41
- package/skills/codex/references/authoring-workflow.md +15 -10
- package/skills/codex/references/cli-commands.md +1 -0
- package/skills/codex/references/compile-errors.md +2 -2
- package/skills/codex/references/connect-an-api.md +2 -2
- package/skills/codex/references/experience-design.md +1 -1
- package/skills/codex/references/sdk-surface.md +16 -16
- package/skills/codex/references/troubleshooting.md +2 -2
- package/skills/codex/references/widgets-and-apps.md +9 -8
|
@@ -155,7 +155,7 @@ All tools are atomic, model-fillable from natural language, and each returns a s
|
|
|
155
155
|
**★ `list_today`** *(read-only · renders `TaskList`)*
|
|
156
156
|
- **Input:** `{ focus: string = "today" }`
|
|
157
157
|
- **Output:** `{ status, focus, tasks: [{ id, title, priority, done }] }`
|
|
158
|
-
- **Notes:** `
|
|
158
|
+
- **Notes:** `tool` — the one tool that opens the widget. Host status copy: invoking "Loading your tasks…", invoked "Tasks ready".
|
|
159
159
|
|
|
160
160
|
**★ `add_task`** *(local write, non-destructive · model-visible)*
|
|
161
161
|
- **Input:** `{ title: string, priority: "high"|"medium"|"low" = "medium" }`
|
|
@@ -170,7 +170,7 @@ All tools are atomic, model-fillable from natural language, and each returns a s
|
|
|
170
170
|
**`set_priority`** *(local write, non-destructive · widget-only)*
|
|
171
171
|
- **Input:** `{ task: string (id), priority: "high"|"medium"|"low" }`
|
|
172
172
|
- **Output:** `{ status, task, priority }`
|
|
173
|
-
- **Notes:** `
|
|
173
|
+
- **Notes:** `tool` — hidden from the model; the `<select>` in `TaskList` is its only caller, keeping the model's tool surface to the three it should reason about.
|
|
174
174
|
|
|
175
175
|
---
|
|
176
176
|
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
<div class="rationale">
|
|
218
218
|
<h4>How to read these wireframes</h4>
|
|
219
219
|
<p>Solid-border phones are the <strong>in-ChatGPT app</strong>. Acme Tasks is a <strong>two-way (read + write) app</strong>, not a discovery funnel: every widget reflects the user's real list, and each action — capture, re-prioritize, complete — is <strong>recorded through a tool call</strong> and confirmed in-chat. The single dashed phone is the <strong>one-time account link</strong> (<span class="mono">customerAuth</span>); there is no transactional off-app step.</p>
|
|
220
|
-
<p>Widgets are built with the Noodle Seed authoring SDK — <span class="mono">
|
|
220
|
+
<p>Widgets are built with the Noodle Seed authoring SDK — <span class="mono">tool + view</span> renders <span class="mono">TaskList</span>; <span class="mono">tool + app visibility</span> powers the in-widget re-prioritize. Styling uses host/Noodle Seed semantic tokens via CSS cascade layers; the brand accent (<span class="mono">#7C3AED</span>) is reserved for the logo, checks, primary CTA and <em>high</em> emphasis. Priority uses fixed semantic colors: high red · medium amber · low slate.</p>
|
|
221
221
|
<p><span class="r-tag ux">UX</span> flow rationale <span class="r-tag ui">UI</span> interface rationale <span class="r-tag acme">ACME</span> product-model fit <span class="r-tag trust">TRUST</span> write-safety / grounding guardrail</p>
|
|
222
222
|
</div>
|
|
223
223
|
</div>
|
|
@@ -401,11 +401,11 @@
|
|
|
401
401
|
<div class="section" id="prioritize">
|
|
402
402
|
<span class="section-label">Flow 2 · the hub</span>
|
|
403
403
|
<h2 class="section-title">Prioritize — read the day, then re-order it in place</h2>
|
|
404
|
-
<p class="section-subtitle"><span class="mono">list_today</span> renders the grounded list, highest priority first; the per-row priority control (a <span class="mono">
|
|
404
|
+
<p class="section-subtitle"><span class="mono">list_today</span> renders the grounded list, highest priority first; the per-row priority control (a <span class="mono">tool + app visibility</span> helper hidden from the model) lets the user re-rank without describing the task twice. This is the widget the other two flows write into.</p>
|
|
405
405
|
<div class="rationale">
|
|
406
406
|
<h4>Why it's built this way</h4>
|
|
407
407
|
<p><span class="r-tag ux">UX</span> "What's on my plate?" wants an answer <em>and</em> a next move. The list is grouped high → low so the triager sees the real order at a glance and adjusts with one tap.</p>
|
|
408
|
-
<p><span class="r-tag acme">ACME</span> Re-prioritize is <span class="mono">set_priority</span> as <span class="mono">
|
|
408
|
+
<p><span class="r-tag acme">ACME</span> Re-prioritize is <span class="mono">set_priority</span> as <span class="mono">tool + app visibility</span> — <strong>widget-only</strong>, hidden from the model's tool surface. The model reasons about three tools (list, add, complete); the fourth is pure UI plumbing, keeping the model's choices clean.</p>
|
|
409
409
|
<p><span class="r-tag trust">TRUST</span> The list only ever shows tasks that exist — the app never invents a task or a priority. Every re-rank echoes a confirmation ("Set review_pr to high priority").</p>
|
|
410
410
|
</div>
|
|
411
411
|
<div class="phones-row">
|
|
@@ -543,7 +543,7 @@
|
|
|
543
543
|
<div class="section" id="api">
|
|
544
544
|
<span class="section-label">Technical appendix</span>
|
|
545
545
|
<h2 class="section-title">MCP tools & call sequence</h2>
|
|
546
|
-
<p class="section-subtitle">The four tools behind the flows. <span class="mono">list_today</span> is the only widget-opening tool (<span class="mono">
|
|
546
|
+
<p class="section-subtitle">The four tools behind the flows. <span class="mono">list_today</span> is the only widget-opening tool (<span class="mono">tool + view</span>); <span class="mono">set_priority</span> is widget-only (<span class="mono">tool + app visibility</span>, hidden from the model); <span class="mono">add_task</span> and <span class="mono">complete_task</span> are model-visible.</p>
|
|
547
547
|
|
|
548
548
|
<div class="api-panel">
|
|
549
549
|
<h4>Read & render (Flow 2 · the hub)</h4>
|
|
@@ -559,7 +559,7 @@
|
|
|
559
559
|
|
|
560
560
|
<div class="api-panel">
|
|
561
561
|
<h4>Prioritize (Flow 2 helper) & Complete (Flow 3)</h4>
|
|
562
|
-
<div class="api-step"><span class="verb">WRITE</span><span><span class="tname">set_priority</span> — returns { status, task, priority } ·
|
|
562
|
+
<div class="api-step"><span class="verb">WRITE</span><span><span class="tname">set_priority</span> — returns { status, task, priority } · tool + app visibility, hidden from the model</span></div>
|
|
563
563
|
<div class="api-note">Input <span class="mono">{ task:id, priority }</span>. Called only by the priority ▾ in TaskList — keeps the model's tool surface to the three it should reason about.</div>
|
|
564
564
|
<div class="api-step"><span class="verb">WRITE</span><span><span class="tname">complete_task</span> — returns { status, task } · model-visible</span></div>
|
|
565
565
|
<div class="api-note">Input <span class="mono">{ task:id, title:"" }</span>. Model-visible so "I finished X" completes directly; local non-destructive write, confirmed with the remaining count.</div>
|
|
@@ -596,7 +596,7 @@
|
|
|
596
596
|
|
|
597
597
|
<div class="footer">
|
|
598
598
|
Acme Tasks × ChatGPT — Two-Way App Wireframes · Noodle Seed · v1 · July 2026<br>
|
|
599
|
-
Illustrative wireframes for a fictional app. Built with the Noodle Seed authoring SDK (
|
|
599
|
+
Illustrative wireframes for a fictional app. Built with the Noodle Seed authoring SDK (tool + view / tool + app visibility · customerAuth). This flagship seeds the task list; a production deployment connects the user's account. Task data shown is sample content.
|
|
600
600
|
</div>
|
|
601
601
|
|
|
602
602
|
</body>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { annotations, server, tool,
|
|
1
|
+
import { annotations, server, tool, z } from '@noodleseed/one';
|
|
2
2
|
|
|
3
3
|
// Acme Tasks is a fictional productivity app. It is a two-way (read + write) experience rather than a
|
|
4
4
|
// top-of-funnel handoff: the top-3 prioritized user flows all complete in chat — Capture, Prioritize,
|
|
@@ -52,7 +52,7 @@ export default server(
|
|
|
52
52
|
},
|
|
53
53
|
[
|
|
54
54
|
// Flow 2 — Prioritize / Today: render the list so the human triages and the model can speak to it.
|
|
55
|
-
|
|
55
|
+
tool('list_today', {
|
|
56
56
|
description: 'Show today’s Acme Tasks and render the task-list widget.',
|
|
57
57
|
annotations: readOnly,
|
|
58
58
|
input: z.object({ focus: z.string().default('today') }),
|
|
@@ -66,8 +66,8 @@ export default server(
|
|
|
66
66
|
focus: input.focus,
|
|
67
67
|
tasks: today,
|
|
68
68
|
}),
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
viewTitle: 'Today’s tasks',
|
|
70
|
+
viewDescription: 'A prioritized task list: capture, re-prioritize, and complete in place.',
|
|
71
71
|
// ChatGPT host status copy (openai/toolInvocation/*) — required for widget-opening tools.
|
|
72
72
|
invoking: 'Loading your tasks…',
|
|
73
73
|
invoked: 'Tasks ready',
|
|
@@ -119,7 +119,8 @@ export default server(
|
|
|
119
119
|
}),
|
|
120
120
|
}),
|
|
121
121
|
// Flow 2 helper (widget-only): re-prioritize a task from the list widget.
|
|
122
|
-
|
|
122
|
+
tool('set_priority', {
|
|
123
|
+
visibility: ['app'],
|
|
123
124
|
description: 'Re-prioritize a task from the list widget.',
|
|
124
125
|
annotations: localWrite,
|
|
125
126
|
input: z.object({
|
|
@@ -1,15 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
annotations,
|
|
3
|
-
asset,
|
|
4
|
-
connector,
|
|
5
|
-
resource,
|
|
6
|
-
server,
|
|
7
|
-
tool,
|
|
8
|
-
toolForWidget,
|
|
9
|
-
toolWithWidget,
|
|
10
|
-
widget,
|
|
11
|
-
z,
|
|
12
|
-
} from '@noodleseed/one';
|
|
1
|
+
import { annotations, asset, connector, resource, server, tool, z } from '@noodleseed/one';
|
|
13
2
|
|
|
14
3
|
const heroImage = asset('assets/noodle-bowl.jpg');
|
|
15
4
|
|
|
@@ -17,28 +6,28 @@ const state = connector('noodle_state')
|
|
|
17
6
|
.version('1.0.0')
|
|
18
7
|
.operation('read_state', {
|
|
19
8
|
type: 'read',
|
|
20
|
-
input: {
|
|
21
|
-
handle:
|
|
22
|
-
key:
|
|
23
|
-
},
|
|
24
|
-
output: {
|
|
25
|
-
value:
|
|
26
|
-
revision:
|
|
27
|
-
status:
|
|
28
|
-
},
|
|
9
|
+
input: z.object({
|
|
10
|
+
handle: z.string(),
|
|
11
|
+
key: z.string().optional(),
|
|
12
|
+
}),
|
|
13
|
+
output: z.object({
|
|
14
|
+
value: z.record(z.string(), z.unknown()),
|
|
15
|
+
revision: z.number().int(),
|
|
16
|
+
status: z.string(),
|
|
17
|
+
}),
|
|
29
18
|
})
|
|
30
19
|
.operation('patch_state', {
|
|
31
20
|
type: 'action',
|
|
32
|
-
input: {
|
|
33
|
-
handle:
|
|
34
|
-
expectedRevision:
|
|
35
|
-
value:
|
|
36
|
-
},
|
|
37
|
-
output: {
|
|
38
|
-
value:
|
|
39
|
-
revision:
|
|
40
|
-
status:
|
|
41
|
-
},
|
|
21
|
+
input: z.object({
|
|
22
|
+
handle: z.string(),
|
|
23
|
+
expectedRevision: z.number().int(),
|
|
24
|
+
value: z.record(z.string(), z.unknown()),
|
|
25
|
+
}),
|
|
26
|
+
output: z.object({
|
|
27
|
+
value: z.record(z.string(), z.unknown()),
|
|
28
|
+
revision: z.number().int(),
|
|
29
|
+
status: z.string(),
|
|
30
|
+
}),
|
|
42
31
|
});
|
|
43
32
|
|
|
44
33
|
const stores = [
|
|
@@ -235,7 +224,7 @@ export default server(
|
|
|
235
224
|
},
|
|
236
225
|
},
|
|
237
226
|
[
|
|
238
|
-
|
|
227
|
+
tool('open_ordering', {
|
|
239
228
|
description:
|
|
240
229
|
'Open a complete food-ordering widget with store discovery, menu browsing, cart review, and checkout handoff.',
|
|
241
230
|
annotations: readOnly,
|
|
@@ -257,13 +246,13 @@ export default server(
|
|
|
257
246
|
featuredItems: menu,
|
|
258
247
|
fallback: 'Open stores: Harbor Noodles (Noodles), Garden Wraps (Vegetarian).',
|
|
259
248
|
}),
|
|
260
|
-
|
|
249
|
+
viewTitle: 'Food ordering',
|
|
261
250
|
domain: 'https://orders.example.com',
|
|
262
251
|
view: {
|
|
263
252
|
component: 'ordering-flow',
|
|
264
253
|
entry: './views/ordering-flow.tsx',
|
|
265
254
|
},
|
|
266
|
-
|
|
255
|
+
viewDescription:
|
|
267
256
|
'A complete consumer ordering surface with app-only helper tools, cart state, and checkout handoff.',
|
|
268
257
|
csp: {
|
|
269
258
|
connectDomains: ['https://orders.example.com'],
|
|
@@ -272,7 +261,8 @@ export default server(
|
|
|
272
261
|
},
|
|
273
262
|
permissions: { clipboardWrite: {} },
|
|
274
263
|
}),
|
|
275
|
-
|
|
264
|
+
tool('search_stores', {
|
|
265
|
+
visibility: ['app'],
|
|
276
266
|
description: 'Filter synthetic restaurants for the ordering widget.',
|
|
277
267
|
annotations: readOnly,
|
|
278
268
|
input: z.object({
|
|
@@ -282,7 +272,8 @@ export default server(
|
|
|
282
272
|
output: z.object({ stores: z.array(storeShape) }),
|
|
283
273
|
fulfil: () => ({ stores }),
|
|
284
274
|
}),
|
|
285
|
-
|
|
275
|
+
tool('load_menu', {
|
|
276
|
+
visibility: ['app'],
|
|
286
277
|
description: 'Load synthetic menu categories and items for one store.',
|
|
287
278
|
annotations: readOnly,
|
|
288
279
|
input: z.object({ storeId: z.string() }),
|
|
@@ -293,14 +284,16 @@ export default server(
|
|
|
293
284
|
}),
|
|
294
285
|
fulfil: ({ input }) => ({ storeId: input.storeId, stores, items: menu }),
|
|
295
286
|
}),
|
|
296
|
-
|
|
287
|
+
tool('load_item', {
|
|
288
|
+
visibility: ['app'],
|
|
297
289
|
description: 'Load item details and modifier options for the ordering widget.',
|
|
298
290
|
annotations: readOnly,
|
|
299
291
|
input: z.object({ itemId: z.string() }),
|
|
300
292
|
output: z.object({ itemId: z.string(), items: z.array(menuItemShape) }),
|
|
301
293
|
fulfil: ({ input }) => ({ itemId: input.itemId, items: menu }),
|
|
302
294
|
}),
|
|
303
|
-
|
|
295
|
+
tool('read_cart', {
|
|
296
|
+
visibility: ['app'],
|
|
304
297
|
description: 'Read the caller-scoped ordering cart state.',
|
|
305
298
|
annotations: readOnly,
|
|
306
299
|
input: z.object({}),
|
|
@@ -314,7 +307,8 @@ export default server(
|
|
|
314
307
|
return { value: state.value, revision: state.revision, status: state.status };
|
|
315
308
|
},
|
|
316
309
|
}),
|
|
317
|
-
|
|
310
|
+
tool('sync_cart', {
|
|
311
|
+
visibility: ['app'],
|
|
318
312
|
description: 'Patch the caller-scoped ordering cart with the widget cart mirror.',
|
|
319
313
|
annotations: action,
|
|
320
314
|
input: cartInput,
|
|
@@ -333,7 +327,8 @@ export default server(
|
|
|
333
327
|
return { cart, revision: state.revision, status: state.status };
|
|
334
328
|
},
|
|
335
329
|
}),
|
|
336
|
-
|
|
330
|
+
tool('prepare_checkout', {
|
|
331
|
+
visibility: ['app'],
|
|
337
332
|
description: 'Prepare the caller-scoped cart for checkout handoff.',
|
|
338
333
|
annotations: action,
|
|
339
334
|
input: cartInput,
|
|
@@ -375,6 +370,17 @@ export default server(
|
|
|
375
370
|
status: 'Food Ordering widget capabilities are ready.',
|
|
376
371
|
note: 'Standalone preview covers React views, helper tools, cart state, handoff, CSP, and permissions.',
|
|
377
372
|
}),
|
|
373
|
+
viewName: 'capabilities_card',
|
|
374
|
+
viewTitle: 'Food Ordering capabilities',
|
|
375
|
+
viewDescription: 'Standalone widget resource for previewing the ordering capability surface.',
|
|
376
|
+
domain: 'https://orders.example.com',
|
|
377
|
+
view: { component: 'capabilities-card', entry: './views/capabilities-card.tsx' },
|
|
378
|
+
csp: {
|
|
379
|
+
connectDomains: ['https://orders.example.com'],
|
|
380
|
+
resourceDomains: ['https://orders.example.com'],
|
|
381
|
+
frameDomains: ['https://orders.example.com'],
|
|
382
|
+
},
|
|
383
|
+
permissions: { clipboardWrite: {} },
|
|
378
384
|
}),
|
|
379
385
|
resource('food_ordering_guide', {
|
|
380
386
|
uri: 'docs://food-ordering',
|
|
@@ -392,21 +398,5 @@ export default server(
|
|
|
392
398
|
'- Checkout opens an allowlisted example URL; payment and final ordering remain out of scope.',
|
|
393
399
|
].join('\n'),
|
|
394
400
|
}),
|
|
395
|
-
widget('capabilities_card', {
|
|
396
|
-
tool: 'show_capabilities',
|
|
397
|
-
title: 'Food Ordering capabilities',
|
|
398
|
-
domain: 'https://orders.example.com',
|
|
399
|
-
description: 'Standalone widget resource for previewing the ordering capability surface.',
|
|
400
|
-
view: {
|
|
401
|
-
component: 'capabilities-card',
|
|
402
|
-
entry: './views/capabilities-card.tsx',
|
|
403
|
-
},
|
|
404
|
-
csp: {
|
|
405
|
-
connectDomains: ['https://orders.example.com'],
|
|
406
|
-
resourceDomains: ['https://orders.example.com'],
|
|
407
|
-
frameDomains: ['https://orders.example.com'],
|
|
408
|
-
},
|
|
409
|
-
permissions: { clipboardWrite: {} },
|
|
410
|
-
}),
|
|
411
401
|
],
|
|
412
402
|
);
|
|
@@ -19,13 +19,13 @@ const geocoding = connector('open_meteo_geocoding')
|
|
|
19
19
|
method: 'GET',
|
|
20
20
|
path: '/v1/search',
|
|
21
21
|
query: ['name'],
|
|
22
|
-
input: { name:
|
|
23
|
-
output: {
|
|
24
|
-
latitude:
|
|
25
|
-
longitude:
|
|
26
|
-
place:
|
|
27
|
-
country:
|
|
28
|
-
},
|
|
22
|
+
input: z.object({ name: z.string() }),
|
|
23
|
+
output: z.object({
|
|
24
|
+
latitude: z.number(),
|
|
25
|
+
longitude: z.number(),
|
|
26
|
+
place: z.string().optional(),
|
|
27
|
+
country: z.string().optional(),
|
|
28
|
+
}),
|
|
29
29
|
response: {
|
|
30
30
|
latitude: '${response.results[0].latitude}',
|
|
31
31
|
longitude: '${response.results[0].longitude}',
|
|
@@ -44,13 +44,8 @@ const geocoding = connector('open_meteo_geocoding')
|
|
|
44
44
|
method: 'GET',
|
|
45
45
|
path: '/v1/search',
|
|
46
46
|
query: ['name', 'count'],
|
|
47
|
-
input: {
|
|
48
|
-
|
|
49
|
-
count: { type: 'number' },
|
|
50
|
-
},
|
|
51
|
-
output: {
|
|
52
|
-
results: { type: 'array' },
|
|
53
|
-
},
|
|
47
|
+
input: z.object({ name: z.string(), count: z.number().optional() }),
|
|
48
|
+
output: z.object({ results: z.array(z.unknown()).optional() }),
|
|
54
49
|
response: {
|
|
55
50
|
results: '${response.results}',
|
|
56
51
|
},
|
|
@@ -69,15 +64,12 @@ const forecast = connector('open_meteo_forecast')
|
|
|
69
64
|
method: 'GET',
|
|
70
65
|
path: '/v1/forecast?current_weather=true',
|
|
71
66
|
query: ['latitude', 'longitude'],
|
|
72
|
-
input: {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
windspeed: { type: 'number' },
|
|
79
|
-
weathercode: { type: 'number' },
|
|
80
|
-
},
|
|
67
|
+
input: z.object({ latitude: z.number(), longitude: z.number() }),
|
|
68
|
+
output: z.object({
|
|
69
|
+
temperature: z.number().optional(),
|
|
70
|
+
windspeed: z.number().optional(),
|
|
71
|
+
weathercode: z.number().optional(),
|
|
72
|
+
}),
|
|
81
73
|
response: {
|
|
82
74
|
temperature: '${response.current_weather.temperature}',
|
|
83
75
|
windspeed: '${response.current_weather.windspeed}',
|
|
@@ -91,18 +83,18 @@ const brief = connector('weather_brief')
|
|
|
91
83
|
.version('1.0.0')
|
|
92
84
|
.compute('summarize', {
|
|
93
85
|
type: 'read',
|
|
94
|
-
input: {
|
|
95
|
-
place:
|
|
96
|
-
country:
|
|
97
|
-
temperature:
|
|
98
|
-
windspeed:
|
|
99
|
-
weathercode:
|
|
100
|
-
},
|
|
101
|
-
output: {
|
|
102
|
-
conditions:
|
|
103
|
-
headline:
|
|
104
|
-
advice:
|
|
105
|
-
},
|
|
86
|
+
input: z.object({
|
|
87
|
+
place: z.string(),
|
|
88
|
+
country: z.string().optional(),
|
|
89
|
+
temperature: z.number(),
|
|
90
|
+
windspeed: z.number(),
|
|
91
|
+
weathercode: z.number(),
|
|
92
|
+
}),
|
|
93
|
+
output: z.object({
|
|
94
|
+
conditions: z.string(),
|
|
95
|
+
headline: z.string(),
|
|
96
|
+
advice: z.string(),
|
|
97
|
+
}),
|
|
106
98
|
// A real function — type-checked here, serialized to source and run in the sandbox. It must be
|
|
107
99
|
// self-contained: no imports, no closure over outer variables, synchronous.
|
|
108
100
|
run: (input) => {
|
|
@@ -162,12 +154,8 @@ const placeNarrow = connector('geo_places')
|
|
|
162
154
|
.version('1.0.0')
|
|
163
155
|
.compute('narrow', {
|
|
164
156
|
type: 'read',
|
|
165
|
-
input: {
|
|
166
|
-
|
|
167
|
-
},
|
|
168
|
-
output: {
|
|
169
|
-
places: { type: 'array', required: true },
|
|
170
|
-
},
|
|
157
|
+
input: z.object({ results: z.unknown().optional() }),
|
|
158
|
+
output: z.object({ places: z.array(z.unknown()) }),
|
|
171
159
|
// Self-contained: no imports, no closure over outer variables, synchronous.
|
|
172
160
|
run: (input) => {
|
|
173
161
|
const raw = input.results;
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- Compute connector example
|
|
12
12
|
- Tests
|
|
13
13
|
- Secrets and variables
|
|
14
|
+
- Embedded assistant
|
|
14
15
|
- Boundaries
|
|
15
16
|
|
|
16
17
|
## Input paths
|
|
@@ -55,16 +56,16 @@ const crm = connector('crm').version('1.0.0').http({
|
|
|
55
56
|
method: 'GET',
|
|
56
57
|
path: '/customers',
|
|
57
58
|
query: ['email'],
|
|
58
|
-
input: { email:
|
|
59
|
-
output: { id:
|
|
59
|
+
input: z.object({ email: z.string() }),
|
|
60
|
+
output: z.object({ id: z.string(), name: z.string().optional() }),
|
|
60
61
|
response: { id: '${response.data[0].id}', name: '${response.data[0].name}' },
|
|
61
62
|
},
|
|
62
63
|
create_ticket: {
|
|
63
64
|
type: 'action',
|
|
64
65
|
method: 'POST',
|
|
65
66
|
path: '/tickets',
|
|
66
|
-
input: { customer_id:
|
|
67
|
-
output: { ticket_id:
|
|
67
|
+
input: z.object({ customer_id: z.string(), body: z.string() }),
|
|
68
|
+
output: z.object({ ticket_id: z.string() }),
|
|
68
69
|
request: { customer_id: '${args.customer_id}', body: '${args.body}' },
|
|
69
70
|
response: { ticket_id: '${response.id}' },
|
|
70
71
|
},
|
|
@@ -120,16 +121,16 @@ const tasks = connector('tasks').version('1.0.0').http({
|
|
|
120
121
|
method: 'GET',
|
|
121
122
|
path: '/tasks',
|
|
122
123
|
query: ['query'],
|
|
123
|
-
input: { query:
|
|
124
|
-
output: { matches:
|
|
124
|
+
input: z.object({ query: z.string() }),
|
|
125
|
+
output: z.object({ matches: z.array(z.unknown()) }),
|
|
125
126
|
response: { matches: '${response.results}' },
|
|
126
127
|
},
|
|
127
128
|
close_task: {
|
|
128
129
|
type: 'action',
|
|
129
130
|
method: 'POST',
|
|
130
131
|
path: '/tasks/{id}/close',
|
|
131
|
-
input: { id:
|
|
132
|
-
output: { ok:
|
|
132
|
+
input: z.object({ id: z.string() }),
|
|
133
|
+
output: z.object({ ok: z.boolean() }),
|
|
133
134
|
response: { ok: '${response.ok}' },
|
|
134
135
|
},
|
|
135
136
|
},
|
|
@@ -163,8 +164,8 @@ The model never sees a task id from the user; `find_tasks` returns `{ id, title
|
|
|
163
164
|
|
|
164
165
|
```ts
|
|
165
166
|
const scoring = connector('scoring').version('1.0.0').compute('normalize', {
|
|
166
|
-
input: { email:
|
|
167
|
-
output: { score:
|
|
167
|
+
input: z.object({ email: z.string(), priority: z.string().optional() }),
|
|
168
|
+
output: z.object({ score: z.number() }),
|
|
168
169
|
calls: { find_customer: 'crm.find_customer' },
|
|
169
170
|
limits: { timeoutMs: 1000, maxHostCalls: 2 },
|
|
170
171
|
run(input, { callOperation }) {
|
|
@@ -197,6 +198,10 @@ After focused tests pass, run `noodle validate --json`, `noodle test --json`, an
|
|
|
197
198
|
|
|
198
199
|
Author managed config as `secret("NAME")` / `variable("NAME")` and operate it with `noodle secrets set` / `noodle variables set` (scoped org/app/env). Never inline secret values in `server.ts`, tests, or generated files.
|
|
199
200
|
|
|
201
|
+
## Embedded assistant
|
|
202
|
+
|
|
203
|
+
To place the same server tools inside the SaaS web app, add `assistant: embeddedAssistant({ model: openAICompatible({ baseUrl: variable("ASSISTANT_MODEL_BASE_URL"), model: variable("ASSISTANT_MODEL"), apiKey: secret("ASSISTANT_MODEL_API_KEY") }), allowedOrigins: ["https://app.example.com"], appearance: { brand: { name: "Acme Assistant" }, theme: { defaultMode: "auto", light: { accent: "#3157D5" }, dark: { accent: "#9FB4FF" } } } })` to the server options. Keep origins exact. The customer backend uses `@noodleseed/assistant/server` to exchange its already-authenticated user for a short-lived browser session; client and model secrets never enter the browser. Run `noodle check --target embedded-assistant` before deploy, then manage backend credentials with `noodle assistant clients create|list|rotate|revoke`.
|
|
204
|
+
|
|
200
205
|
## Boundaries
|
|
201
206
|
|
|
202
207
|
Do not hand-author manifest JSON/YAML, runtime artifacts, connector IR, or hosted asset metadata. Do not read or copy secrets, bearer tokens, refresh tokens, static access keys, `.env.noodle`, or `~/.noodle/config.json`. Hosted access is identity-based — do not add static data-plane credential paths.
|
|
@@ -46,6 +46,7 @@ Every `noodle` command, grouped by area. Local authoring commands (`validate`, `
|
|
|
46
46
|
| Command | What it does |
|
|
47
47
|
| :-- | :-- |
|
|
48
48
|
| `noodle link` | Bind this directory to a Noodle Seed Cloud target (org/app/env). |
|
|
49
|
+
| `noodle assistant` | Manage backend credentials for customer-branded embedded assistant clients. |
|
|
49
50
|
| `noodle deploy` | Deploy the server to Noodle Seed Cloud. |
|
|
50
51
|
| `noodle open` | Open or print the latest deployment URL. |
|
|
51
52
|
| `noodle status` | Show hosted deployment status. |
|
|
@@ -38,8 +38,8 @@ Run `noodle validate` (add `--json` for the machine-readable envelope, `--fix-pr
|
|
|
38
38
|
| `duplicate_prompt` | Two prompts share a name; rename one `prompt(...)`. |
|
|
39
39
|
| `duplicate_resource_uri` | Two resources resolve to the same URI; make each resource URI unique. |
|
|
40
40
|
| `unsupported_uri_template` | Fix the resource URI template to a supported form at the cited `path`. |
|
|
41
|
-
| `duplicate_widget` | Two
|
|
42
|
-
| `unknown_widget_tool` | The widget references a tool that does not exist; point `
|
|
41
|
+
| `duplicate_widget` | Two tool views share an identity; give each `viewName` or `view.component` a unique name. |
|
|
42
|
+
| `unknown_widget_tool` | The widget references a tool that does not exist; point `tool`/`view` at a declared tool (see `didYouMean`). |
|
|
43
43
|
| `unknown_widget_action_tool` | A widget action calls a tool that is not declared; declare it or fix the action target name. |
|
|
44
44
|
| `duplicate_widget_tool` | A tool is bound to more than one widget; bind each tool to a single widget. |
|
|
45
45
|
| `invalid_widget_binding` | Fix the `data-bind`/binding expression in the widget; it does not resolve against the tool output. |
|
|
@@ -62,7 +62,7 @@ Most real tools return a variable-length list (search results, a user’s tasks)
|
|
|
62
62
|
|
|
63
63
|
```ts
|
|
64
64
|
// The API returns { results: [ { id, name, country, … }, … ] }
|
|
65
|
-
output: { places:
|
|
65
|
+
output: z.object({ places: z.array(z.unknown()) }),
|
|
66
66
|
response: { places: '${response.results}' },
|
|
67
67
|
```
|
|
68
68
|
|
|
@@ -96,7 +96,7 @@ Pair the read/list with the mutations your intent tools need:
|
|
|
96
96
|
```ts
|
|
97
97
|
close_task: {
|
|
98
98
|
type: 'action', method: 'POST', path: '/tasks/{id}/close',
|
|
99
|
-
input: { id:
|
|
99
|
+
input: z.object({ id: z.string() }),
|
|
100
100
|
responseType: 'empty',
|
|
101
101
|
},
|
|
102
102
|
```
|
|
@@ -87,7 +87,7 @@ the widget**, and the widget filled with plausible, internally consistent data (
|
|
|
87
87
|
Label each widget with its component name so the wireframe, the spec, and the code share one
|
|
88
88
|
vocabulary; put the funnel boundary at the top; and render off-app destinations distinctly (they are
|
|
89
89
|
reached only after the handoff). Each wireframe screen maps directly to noodle: a screen with a widget
|
|
90
|
-
is a `
|
|
90
|
+
is a `tool` + a React `view`; a plain answer is a `tool`; an off-app destination is a
|
|
91
91
|
`handoff.allowedDomains` entry. A compact single screen, anonymized to a fictional "Acme" business:
|
|
92
92
|
|
|
93
93
|
```html
|
|
@@ -14,13 +14,10 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
|
|
|
14
14
|
### Server & tools
|
|
15
15
|
|
|
16
16
|
- `server(name, options, definitions)` — the server/app root.
|
|
17
|
-
- `tool(name,
|
|
18
|
-
- `toolWithWidget(name, { ..., view })` — a model-visible tool that renders an MCP Apps widget.
|
|
19
|
-
- `toolForWidget(name, { ... })` — a widget-only helper tool, hidden from the model.
|
|
17
|
+
- `tool(name, options)` — declare every tool; add `view` to render an MCP App or `visibility: ["app"]` for an app-only helper.
|
|
20
18
|
|
|
21
|
-
###
|
|
19
|
+
### Views & assets
|
|
22
20
|
|
|
23
|
-
- `widget(...)` — declare a widget/view component.
|
|
24
21
|
- `asset("./path")` — reference a packaged asset (e.g. an image).
|
|
25
22
|
- `annotations(...)` — tool/Apps annotation metadata.
|
|
26
23
|
|
|
@@ -51,14 +48,17 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
|
|
|
51
48
|
|
|
52
49
|
- `z` — Zod, for input/output schemas (compiles to JSON Schema 2020-12).
|
|
53
50
|
|
|
51
|
+
### Other
|
|
52
|
+
|
|
53
|
+
- `embeddedAssistant`
|
|
54
|
+
- `openAICompatible`
|
|
55
|
+
|
|
54
56
|
## Authoring signatures
|
|
55
57
|
|
|
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
|
|
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.
|
|
58
|
+
- `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.
|
|
59
|
+
- `tool(name, { description, input, output, annotations?, visibility?, view?, fulfil })` — `input`/`output` are Zod schemas; `fulfil({ input, connectors, user })` returns data matching `output`. Add `view: { component, entry }` for a React widget; use `visibility: ["app"]` for an app-only helper.
|
|
60
60
|
- `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
|
|
61
|
-
-
|
|
61
|
+
- View metadata (`viewTitle`, `viewDescription`, `csp`, `domain`, `permissions`) belongs on the tool that renders it; `asset("./path")` packages local files.
|
|
62
62
|
- `customerAuth.*(...)` belongs in `server` options when deployed customer callers need verified identity; inspect `examples/customer-auth` or `examples/sharepoint` before using it.
|
|
63
63
|
- `state` defines durable widget state handles; `handoff` declares allowed external domains for safe host handoff.
|
|
64
64
|
|
|
@@ -144,8 +144,8 @@ const crm = connector('crm')
|
|
|
144
144
|
method: 'GET',
|
|
145
145
|
path: '/tickets',
|
|
146
146
|
query: ['id'],
|
|
147
|
-
input: { id:
|
|
148
|
-
output: { subject:
|
|
147
|
+
input: z.object({ id: z.string() }),
|
|
148
|
+
output: z.object({ subject: z.string().optional(), status: z.string().optional() }),
|
|
149
149
|
response: { subject: '${response.subject}', status: '${response.status}' },
|
|
150
150
|
},
|
|
151
151
|
},
|
|
@@ -195,8 +195,8 @@ const orders = connector('orders')
|
|
|
195
195
|
method: 'GET',
|
|
196
196
|
path: '/orders',
|
|
197
197
|
query: ['id'],
|
|
198
|
-
input: { id:
|
|
199
|
-
output: { id:
|
|
198
|
+
input: z.object({ id: z.string() }),
|
|
199
|
+
output: z.object({ id: z.string().optional(), status: z.string().optional() }),
|
|
200
200
|
response: { id: '${response.id}', status: '${response.status}' },
|
|
201
201
|
},
|
|
202
202
|
get_tracking: {
|
|
@@ -204,8 +204,8 @@ const orders = connector('orders')
|
|
|
204
204
|
method: 'GET',
|
|
205
205
|
path: '/tracking',
|
|
206
206
|
query: ['order_id'],
|
|
207
|
-
input: { order_id:
|
|
208
|
-
output: { url:
|
|
207
|
+
input: z.object({ order_id: z.string() }),
|
|
208
|
+
output: z.object({ url: z.string().optional() }),
|
|
209
209
|
response: { url: '${response.url}' },
|
|
210
210
|
},
|
|
211
211
|
},
|
|
@@ -19,9 +19,9 @@ For protocol/conformance checks, the headless harness is `@mcpjam/cli`, not a `n
|
|
|
19
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
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
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 `
|
|
22
|
+
| Tool succeeds but no widget appears | The tool has no view, or the host surface doesn’t support MCP Apps | Use `tool`, run `noodle check`, preview with `noodle devtools`; on non-Apps surfaces only the text/structured result renders |
|
|
23
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 `
|
|
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 `tool` |
|
|
25
25
|
| `noodle validate` passes but React views fail to bundle (“requires Vite”) | Project dependencies are not installed — widget bundling uses the app-local Vite | Run the project’s package install, then retry `noodle dev` / `noodle deploy` |
|
|
26
26
|
| Hosted endpoint returns 401 to probes | Expected: hosted servers challenge unauthenticated calls with OAuth metadata | Sign in from the host when prompted; widen who may call with `noodle access set` if testers are outside the org |
|
|
27
27
|
| Tools error only after deploy | Runtime/config differences surface hosted (secrets, connector reachability) | Run `noodle smoke`, then `noodle metrics --agent-output` and `noodle events --tool <name> --status tool_error --json`; check `noodle secrets list` scope |
|