@noodleseed/agent-kit 0.44.0 → 0.44.1

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 (40) hide show
  1. package/manifest.json +275 -243
  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 +1 -1
  5. package/skills/claude-code/building-mcp-apps/SKILL.md +1 -1
  6. package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +1 -1
  7. package/skills/claude-code/debugging-mcp-delivery/SKILL.md +1 -1
  8. package/skills/claude-code/deploying-mcp-services/SKILL.md +1 -1
  9. package/skills/claude-code/designing-mcp-products/SKILL.md +1 -1
  10. package/skills/claude-code/embedding-mcp-assistants/SKILL.md +1 -1
  11. package/skills/claude-code/examples/food-ordering/README.md +1 -0
  12. package/skills/claude-code/examples/food-ordering/package.json +1 -1
  13. package/skills/claude-code/examples/food-ordering/src/helpers.ts +1 -0
  14. package/skills/claude-code/examples/food-ordering/src/views/ordering-flow.tsx +31 -27
  15. package/skills/claude-code/examples/food-ordering/src/views/ordering-result.ts +84 -0
  16. package/skills/claude-code/examples/food-ordering/test/ordering-flow.test.ts +187 -0
  17. package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
  18. package/skills/claude-code/publishing-mcp-integrations/SKILL.md +1 -1
  19. package/skills/claude-code/references/widgets-and-apps.md +30 -10
  20. package/skills/claude-code/reporting-noodle-feedback/SKILL.md +1 -1
  21. package/skills/claude-code/verifying-mcp-delivery/SKILL.md +1 -1
  22. package/skills/codex/SKILL.md +1 -1
  23. package/skills/codex/authoring-mcp-servers/SKILL.md +1 -1
  24. package/skills/codex/building-mcp-apps/SKILL.md +1 -1
  25. package/skills/codex/connecting-apis-to-mcp/SKILL.md +1 -1
  26. package/skills/codex/debugging-mcp-delivery/SKILL.md +1 -1
  27. package/skills/codex/deploying-mcp-services/SKILL.md +1 -1
  28. package/skills/codex/designing-mcp-products/SKILL.md +1 -1
  29. package/skills/codex/embedding-mcp-assistants/SKILL.md +1 -1
  30. package/skills/codex/examples/food-ordering/README.md +1 -0
  31. package/skills/codex/examples/food-ordering/package.json +1 -1
  32. package/skills/codex/examples/food-ordering/src/helpers.ts +1 -0
  33. package/skills/codex/examples/food-ordering/src/views/ordering-flow.tsx +31 -27
  34. package/skills/codex/examples/food-ordering/src/views/ordering-result.ts +84 -0
  35. package/skills/codex/examples/food-ordering/test/ordering-flow.test.ts +187 -0
  36. package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
  37. package/skills/codex/publishing-mcp-integrations/SKILL.md +1 -1
  38. package/skills/codex/references/widgets-and-apps.md +30 -10
  39. package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
  40. package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/agent-kit",
3
- "version": "0.44.0",
3
+ "version": "0.44.1",
4
4
  "private": false,
5
5
  "description": "Self-checking, self-updating agent skills for the Noodle Seed CLI. Authored in this repo by @noodle-borg/agent-kit; this is the published, independently-versioned canonical skills artifact the CLI fetches and verifies.",
6
6
  "license": "Apache-2.0",
@@ -3,7 +3,7 @@ name: noodle-seed
3
3
  description: "Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:cd6ca0d915e6acb9 -->
6
+ <!-- noodle-skill version:0.44.1 hash:cd6ca0d915e6acb9 -->
7
7
 
8
8
  # Noodle Seed
9
9
 
@@ -3,7 +3,7 @@ name: authoring-mcp-servers
3
3
  description: "Use when creating or extending a headless Noodle Seed MCP server, tool, resource, prompt, or typed model-facing capability."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:0b2fd8c7e43fc69f -->
6
+ <!-- noodle-skill version:0.44.1 hash:0b2fd8c7e43fc69f -->
7
7
 
8
8
  # authoring-mcp-servers
9
9
 
@@ -3,7 +3,7 @@ name: building-mcp-apps
3
3
  description: "Use when a Noodle Seed MCP App, widget, interactive card, visual interaction, or host-visible UI is the primary requested outcome."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:f7fa54992c8d7692 -->
6
+ <!-- noodle-skill version:0.44.1 hash:f7fa54992c8d7692 -->
7
7
 
8
8
  # building-mcp-apps
9
9
 
@@ -3,7 +3,7 @@ name: connecting-apis-to-mcp
3
3
  description: "Use when credentials, an API URL, an OpenAPI document, or an observed response must become real Noodle Seed MCP behavior."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:1e86b8704f407bd3 -->
6
+ <!-- noodle-skill version:0.44.1 hash:1e86b8704f407bd3 -->
7
7
 
8
8
  # connecting-apis-to-mcp
9
9
 
@@ -3,7 +3,7 @@ name: debugging-mcp-delivery
3
3
  description: "Use when an existing Noodle Seed MCP project has a concrete validation, runtime, connector, App, host, deployment, or production failure."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:aa715bae12041d7c -->
6
+ <!-- noodle-skill version:0.44.1 hash:aa715bae12041d7c -->
7
7
 
8
8
  # debugging-mcp-delivery
9
9
 
@@ -3,7 +3,7 @@ name: deploying-mcp-services
3
3
  description: "Use when the user explicitly requests a Noodle Seed hosted link, configuration write, deployment, access change, rollback, or connection write."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:93e735b7ffb45df1 -->
6
+ <!-- noodle-skill version:0.44.1 hash:93e735b7ffb45df1 -->
7
7
 
8
8
  # deploying-mcp-services
9
9
 
@@ -3,7 +3,7 @@ name: designing-mcp-products
3
3
  description: "Use when a Noodle Seed MCP product idea needs conversational fit, user benefit, scope, interaction, or evidence design before implementation."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:76cce86729cffbee -->
6
+ <!-- noodle-skill version:0.44.1 hash:76cce86729cffbee -->
7
7
 
8
8
  # designing-mcp-products
9
9
 
@@ -3,7 +3,7 @@ name: embedding-mcp-assistants
3
3
  description: "Use when embedding a Noodle assistant into an existing SaaS or web application with browser, identity, session, and credential boundaries."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:5d8f40f904d6ab4b -->
6
+ <!-- noodle-skill version:0.44.1 hash:5d8f40f904d6ab4b -->
7
7
 
8
8
  # embedding-mcp-assistants
9
9
 
@@ -24,6 +24,7 @@ private customer data.
24
24
  | Model-visible widget state | `useUpdateModelContext` publishes one cohesive replacement snapshot when supported; `useWidgetLifecycle` auto-publishes mounted/cancelled/dismissed and reports author-owned submitted milestones for future context (not host-presentation proof), while the user-triggered submit pairs `useSendFollowUpMessage` for an immediate reply |
25
25
  | Handoff | `handoff.allowedDomains` allows only `https://orders.example.com` checkout URLs |
26
26
  | Progressive enhancement | Non-Apps hosts still receive stores, featured items, and a readable fallback summary |
27
+ | Fail-closed hydration | The React view treats only the unhydrated, pre-result `{}` envelope as pending; a hydrated empty success remains distinct. It surfaces `isError`, validates required records and identifiers, and withholds ordering actions from malformed results |
27
28
 
28
29
  The example is intentionally richer than the generated starter, but each inline view still follows the
29
30
  same default: one immediate purpose, one primary action, at most one subordinate action, and progressive
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "test": "vitest run test/server.test.ts",
7
+ "test": "vitest run test",
8
8
  "validate": "noodle validate",
9
9
  "dev": "noodle dev",
10
10
  "deploy": "noodle deploy"
@@ -8,6 +8,7 @@ export {
8
8
  createViewStore,
9
9
  DataCard,
10
10
  DataList,
11
+ Feedback,
11
12
  Field,
12
13
  HandoffButton,
13
14
  QuantityStepper,
@@ -7,6 +7,7 @@ import {
7
7
  createViewStore,
8
8
  DataCard,
9
9
  DataList,
10
+ Feedback,
10
11
  Field,
11
12
  HandoffButton,
12
13
  QuantityStepper,
@@ -25,30 +26,11 @@ import {
25
26
  View,
26
27
  ViewStack,
27
28
  } from '../helpers.js';
29
+ import { isOrderingEntryResult, type MenuItem, type Store } from './ordering-result.js';
28
30
  import './widget-style.css';
29
31
 
30
32
  type ViewName = 'stores' | 'menu' | 'item' | 'cart' | 'review' | 'handoff';
31
33
 
32
- type Store = {
33
- readonly id: string;
34
- readonly name: string;
35
- readonly cuisine: string;
36
- readonly address: string;
37
- readonly open: boolean;
38
- readonly etaMinutes: number;
39
- readonly rating: number;
40
- };
41
-
42
- type MenuItem = {
43
- readonly id: string;
44
- readonly storeId: string;
45
- readonly category: string;
46
- readonly name: string;
47
- readonly price: number;
48
- readonly description: string;
49
- readonly modifiers: readonly string[];
50
- };
51
-
52
34
  type CartLine = {
53
35
  readonly itemId: string;
54
36
  readonly quantity: number;
@@ -93,12 +75,11 @@ function modifierLabel(value: string): string {
93
75
 
94
76
  export default function OrderingFlow() {
95
77
  const { displayMode, supports, theme } = useLayout();
96
- const entry = structured<{
97
- readonly customer?: string;
98
- readonly stores?: readonly Store[];
99
- readonly featuredItems?: readonly MenuItem[];
100
- readonly status?: string;
101
- }>(useToolInfo('open_ordering'));
78
+ const toolInfo = useToolInfo('open_ordering');
79
+ const isPending = Object.keys(toolInfo).length === 0;
80
+ const entry = isOrderingEntryResult(toolInfo.structuredContent)
81
+ ? toolInfo.structuredContent
82
+ : undefined;
102
83
  const flow = useAppFlow<ViewName>({
103
84
  key: 'ordering_flow',
104
85
  initialView: 'stores',
@@ -178,7 +159,7 @@ export default function OrderingFlow() {
178
159
  const canUpdateModelContext = supports?.modelContext === true;
179
160
 
180
161
  useEffect(() => {
181
- if (!canUpdateModelContext) return;
162
+ if (!entry || !canUpdateModelContext) return;
182
163
  // Each call replaces model context, so publish one cohesive snapshot of current surface state.
183
164
  void updateModelContext({
184
165
  content: [{ type: 'text', text: `Food ordering: ${llmSummary}; ${lifecycle}.` }],
@@ -199,6 +180,7 @@ export default function OrderingFlow() {
199
180
  cart.lines.length,
200
181
  cart.subtotal,
201
182
  customer,
183
+ entry,
202
184
  lifecycle,
203
185
  llmSummary,
204
186
  selectedItemId,
@@ -304,6 +286,28 @@ export default function OrderingFlow() {
304
286
  }
305
287
  }
306
288
 
289
+ if (isPending) {
290
+ return (
291
+ <Feedback status="loading" title="Loading">
292
+ Loading food ordering…
293
+ </Feedback>
294
+ );
295
+ }
296
+ if (toolInfo.isError) {
297
+ return (
298
+ <Feedback status="error" title="Ordering unavailable">
299
+ Could not load food ordering.
300
+ </Feedback>
301
+ );
302
+ }
303
+ if (!entry) {
304
+ return (
305
+ <Feedback status="error" title="Invalid ordering result">
306
+ Food ordering result was incomplete.
307
+ </Feedback>
308
+ );
309
+ }
310
+
307
311
  return (
308
312
  <AppShell
309
313
  className={`nw-shell${theme === 'dark' ? ' dark' : ''}`}
@@ -0,0 +1,84 @@
1
+ export type Store = {
2
+ readonly id: string;
3
+ readonly name: string;
4
+ readonly cuisine: string;
5
+ readonly address: string;
6
+ readonly open: boolean;
7
+ readonly etaMinutes: number;
8
+ readonly rating: number;
9
+ };
10
+
11
+ export type MenuItem = {
12
+ readonly id: string;
13
+ readonly storeId: string;
14
+ readonly category: string;
15
+ readonly name: string;
16
+ readonly price: number;
17
+ readonly description: string;
18
+ readonly modifiers: readonly string[];
19
+ };
20
+
21
+ export type OrderingEntryResult = {
22
+ readonly customer: string;
23
+ readonly stores: readonly Store[];
24
+ readonly featuredItems: readonly MenuItem[];
25
+ readonly status: string;
26
+ };
27
+
28
+ function nonEmptyString(value: unknown): value is string {
29
+ return typeof value === 'string' && value.trim().length > 0;
30
+ }
31
+
32
+ function isStore(value: unknown): value is Store {
33
+ if (value === null || typeof value !== 'object') return false;
34
+ const store = value as Partial<Store>;
35
+ return (
36
+ nonEmptyString(store.id) &&
37
+ nonEmptyString(store.name) &&
38
+ nonEmptyString(store.cuisine) &&
39
+ nonEmptyString(store.address) &&
40
+ typeof store.open === 'boolean' &&
41
+ typeof store.etaMinutes === 'number' &&
42
+ Number.isFinite(store.etaMinutes) &&
43
+ typeof store.rating === 'number' &&
44
+ Number.isFinite(store.rating)
45
+ );
46
+ }
47
+
48
+ function isMenuItem(value: unknown): value is MenuItem {
49
+ if (value === null || typeof value !== 'object') return false;
50
+ const item = value as Partial<MenuItem>;
51
+ return (
52
+ nonEmptyString(item.id) &&
53
+ nonEmptyString(item.storeId) &&
54
+ nonEmptyString(item.category) &&
55
+ nonEmptyString(item.name) &&
56
+ typeof item.price === 'number' &&
57
+ Number.isFinite(item.price) &&
58
+ nonEmptyString(item.description) &&
59
+ Array.isArray(item.modifiers) &&
60
+ item.modifiers.every(nonEmptyString)
61
+ );
62
+ }
63
+
64
+ export function isOrderingEntryResult(value: unknown): value is OrderingEntryResult {
65
+ if (value === null || typeof value !== 'object') return false;
66
+ const entry = value as Partial<OrderingEntryResult>;
67
+ if (
68
+ !nonEmptyString(entry.customer) ||
69
+ !nonEmptyString(entry.status) ||
70
+ !Array.isArray(entry.stores) ||
71
+ !entry.stores.every(isStore) ||
72
+ !Array.isArray(entry.featuredItems) ||
73
+ !entry.featuredItems.every(isMenuItem)
74
+ ) {
75
+ return false;
76
+ }
77
+ const storeIds = new Set(entry.stores.map((store) => store.id));
78
+ const itemIds = new Set(entry.featuredItems.map((item) => item.id));
79
+ return (
80
+ storeIds.size === entry.stores.length &&
81
+ itemIds.size === entry.featuredItems.length &&
82
+ entry.featuredItems.every((item) => storeIds.has(item.storeId))
83
+ );
84
+ }
@@ -0,0 +1,187 @@
1
+ // @vitest-environment happy-dom
2
+ /// <reference lib="dom" />
3
+ import { act, createElement as h } from 'react';
4
+ import { createRoot, type Root } from 'react-dom/client';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+ import OrderingFlow from '../src/views/ordering-flow.js';
7
+
8
+ vi.mock('../src/helpers.js', async () => {
9
+ const { createElement } = await import('react');
10
+ const container = ({ children }: { readonly children?: unknown }) =>
11
+ createElement('div', null, children);
12
+ const button = ({ children }: { readonly children?: unknown }) =>
13
+ createElement('button', { type: 'button' }, children);
14
+ return {
15
+ ActionBar: container,
16
+ AppShell: ({
17
+ children,
18
+ footer,
19
+ subtitle,
20
+ title,
21
+ }: {
22
+ readonly children?: unknown;
23
+ readonly footer?: unknown;
24
+ readonly subtitle?: string;
25
+ readonly title?: string;
26
+ }) => createElement('main', null, title, subtitle, children, footer),
27
+ AsyncBoundary: container,
28
+ ChoiceGroup: container,
29
+ createViewStore:
30
+ <T>(_key: string, initial: T) =>
31
+ (selector?: (state: T) => unknown) => ({
32
+ state: initial,
33
+ selected: selector?.(initial),
34
+ setState: () => undefined,
35
+ }),
36
+ DataCard: button,
37
+ DataList: container,
38
+ Feedback: ({ children, status }: { readonly children?: unknown; readonly status?: string }) =>
39
+ createElement('div', { 'data-status': status }, children),
40
+ Field: container,
41
+ HandoffButton: button,
42
+ QuantityStepper: container,
43
+ ShellNav: container,
44
+ StatusBadge: container,
45
+ SubmitButton: button,
46
+ useAppFlow: () => ({
47
+ activeView: 'stores',
48
+ navigate: () => undefined,
49
+ back: () => undefined,
50
+ canBack: false,
51
+ params: {},
52
+ }),
53
+ useCallTool: () => ({
54
+ status: 'idle',
55
+ isIdle: true,
56
+ isPending: false,
57
+ isSuccess: false,
58
+ isError: false,
59
+ callTool: () => Promise.resolve({ structuredContent: {} }),
60
+ callToolAsync: () => Promise.resolve({ structuredContent: {} }),
61
+ reset: () => undefined,
62
+ }),
63
+ useHandoff: () => ({ status: 'idle', open: () => Promise.resolve() }),
64
+ useLayout: () => ({
65
+ theme: 'light',
66
+ displayMode: 'inline',
67
+ supports: { modelContext: false, followUpMessage: false },
68
+ }),
69
+ useSendFollowUpMessage: () => () => Promise.resolve(),
70
+ useToolInfo: () => toolResult,
71
+ useUpdateModelContext: () => () => Promise.resolve(),
72
+ useViewState: <T>(_key: string, initial: T) => [initial, () => undefined] as const,
73
+ useWidgetLifecycle: () => () => Promise.resolve(),
74
+ View: container,
75
+ ViewStack: container,
76
+ };
77
+ });
78
+
79
+ type ToolResult = {
80
+ readonly content?: unknown;
81
+ readonly structuredContent?: unknown;
82
+ readonly _meta?: unknown;
83
+ readonly isError?: boolean;
84
+ };
85
+
86
+ let toolResult: ToolResult;
87
+ let root: Root | undefined;
88
+
89
+ beforeEach(() => {
90
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
91
+ toolResult = {};
92
+ document.body.innerHTML = '<div id="root"></div>';
93
+ (globalThis as { __noodleReactVersion?: number }).__noodleReactVersion = 0;
94
+ (globalThis as { __noodleReactBridge?: unknown }).__noodleReactBridge = {
95
+ getToolResult: () => toolResult,
96
+ getViewState: () => ({}),
97
+ setWidgetState: () => undefined,
98
+ getLayout: () => ({
99
+ theme: 'light',
100
+ displayMode: 'inline',
101
+ supports: { modelContext: false, followUpMessage: false },
102
+ }),
103
+ callServerTool: () => Promise.resolve({ structuredContent: {} }),
104
+ openExternal: () => Promise.resolve(),
105
+ sendFollowUpMessage: () => Promise.resolve(),
106
+ updateModelContext: () => Promise.resolve(),
107
+ };
108
+ });
109
+
110
+ afterEach(() => {
111
+ if (root) act(() => root?.unmount());
112
+ root = undefined;
113
+ delete (globalThis as { __noodleReactBridge?: unknown }).__noodleReactBridge;
114
+ });
115
+
116
+ function render(result: ToolResult): string {
117
+ toolResult = result;
118
+ root = createRoot(document.querySelector('#root') as HTMLElement);
119
+ act(() => root?.render(h(OrderingFlow)));
120
+ return document.body.textContent ?? '';
121
+ }
122
+
123
+ describe('food-ordering invoking result states', () => {
124
+ it('renders pending without identifier-dependent actions before hydration', () => {
125
+ const text = render({});
126
+ expect(text).toContain('Loading food ordering');
127
+ expect(text).not.toContain('Search stores');
128
+ expect(document.querySelector('button')).toBeNull();
129
+ });
130
+
131
+ it('renders an explicit tool error without ordering actions', () => {
132
+ const text = render({
133
+ content: [{ type: 'text', text: 'Ordering is unavailable' }],
134
+ isError: true,
135
+ });
136
+ expect(text).toContain('Could not load food ordering');
137
+ expect(text).not.toContain('Search stores');
138
+ expect(document.querySelector('button')).toBeNull();
139
+ });
140
+
141
+ it('rejects malformed success data before rendering identifier-dependent actions', () => {
142
+ const text = render({
143
+ structuredContent: {
144
+ status: 'Ready',
145
+ customer: 'Asha',
146
+ stores: [{ name: 'Missing identifier' }],
147
+ featuredItems: [],
148
+ },
149
+ });
150
+ expect(text).toContain('Food ordering result was incomplete');
151
+ expect(text).not.toContain('Search stores');
152
+ expect(document.querySelector('button')).toBeNull();
153
+ });
154
+
155
+ it('preserves the ordering flow after valid hydration', () => {
156
+ const text = render({
157
+ structuredContent: {
158
+ status: 'Ready to build a food order.',
159
+ customer: 'Asha',
160
+ stores: [
161
+ {
162
+ id: 'harbor-noodles',
163
+ name: 'Harbor Noodles',
164
+ cuisine: 'Noodles',
165
+ address: '18 Pier Lane',
166
+ open: true,
167
+ etaMinutes: 24,
168
+ rating: 4.8,
169
+ },
170
+ ],
171
+ featuredItems: [
172
+ {
173
+ id: 'spicy_miso',
174
+ storeId: 'harbor-noodles',
175
+ category: 'Bowls',
176
+ name: 'Spicy Miso Bowl',
177
+ price: 16,
178
+ description: 'Miso broth and noodles.',
179
+ modifiers: ['extra_noodles'],
180
+ },
181
+ ],
182
+ },
183
+ });
184
+ expect(text).toContain('Search stores');
185
+ expect(text).toContain('Harbor Noodles');
186
+ });
187
+ });
@@ -3,7 +3,7 @@ name: executing-noodle-plans
3
3
  description: "Use when the user asks to execute an approved, decision-complete implementation plan for a Noodle Seed project task by task with test-first changes, review, recovery, and final verification."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:6a9f132ddb79352e -->
6
+ <!-- noodle-skill version:0.44.1 hash:6a9f132ddb79352e -->
7
7
 
8
8
  # Execute a Noodle Seed implementation plan
9
9
 
@@ -3,7 +3,7 @@ name: publishing-mcp-integrations
3
3
  description: "Use when preparing, reviewing, or submitting a Noodle Seed MCP integration to a host or app directory."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:efffbf82007f935d -->
6
+ <!-- noodle-skill version:0.44.1 hash:efffbf82007f935d -->
7
7
 
8
8
  # publishing-mcp-integrations
9
9
 
@@ -26,13 +26,15 @@ Treat 1 MiB of uncompressed UTF-8 HTML as the recommended initial React-widget b
26
26
 
27
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
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
+
29
31
  ## React hook surface
30
32
 
31
33
  Author views as React components. `generateHelpers<ServerDefinition>()` (from `@noodleseed/one/react`) returns the typed host hooks:
32
34
 
33
35
  | Hook | Use for |
34
36
  | :-- | :-- |
35
- | `useToolInfo` | Read the invoking tool result; `structuredContent` is the widget’s typed data payload. |
37
+ | `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. |
36
38
  | `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. |
37
39
  | `useViewState` | Persist per-widget UI state across re-renders and restores: `const [value, setValue] = useViewState("key", initial)`. |
38
40
  | `useLayout` | Read host layout: `{ theme, displayMode, locale?, host?, supports? }` (`displayMode` is `"inline"`/`"pip"`/`"fullscreen"`) — adapt styling to the host theme and mode. |
@@ -65,14 +67,27 @@ const { useToolInfo, useCallTool, useViewState, useLayout, useOpenExternal, useS
65
67
  generateHelpers<ServerDefinition>();
66
68
 
67
69
  type OrderResult = {
68
- readonly customer?: string;
69
- readonly item?: string;
70
- readonly total?: number;
71
- readonly checkoutUrl?: string;
70
+ readonly customer: string;
71
+ readonly item: string;
72
+ readonly total: number;
73
+ readonly checkoutUrl: string;
72
74
  };
73
75
 
76
+ function isOrderResult(value: unknown): value is OrderResult {
77
+ if (value === null || typeof value !== 'object') return false;
78
+ const result = value as Partial<OrderResult>;
79
+ return (
80
+ typeof result.customer === 'string' && result.customer.length > 0 &&
81
+ typeof result.item === 'string' && result.item.length > 0 &&
82
+ typeof result.total === 'number' && Number.isFinite(result.total) &&
83
+ typeof result.checkoutUrl === 'string' && result.checkoutUrl.startsWith('https://')
84
+ );
85
+ }
86
+
74
87
  export default function OrderStatus() {
75
- const shown = useToolInfo('show_order').structuredContent as OrderResult | undefined;
88
+ const toolInfo = useToolInfo('show_order');
89
+ const isPending = Object.keys(toolInfo).length === 0;
90
+ const shown = isOrderResult(toolInfo.structuredContent) ? toolInfo.structuredContent : undefined;
76
91
  const placeOrder = useCallTool('place_order'); // calls the widget-only helper tool
77
92
  const { supports } = useLayout();
78
93
  const openExternal = useOpenExternal();
@@ -85,15 +100,16 @@ export default function OrderStatus() {
85
100
  const checkoutUrl = shown?.checkoutUrl ?? '';
86
101
 
87
102
  useEffect(() => {
88
- if (!supports?.modelContext) return;
103
+ if (!shown || !supports?.modelContext) return;
89
104
  void updateModelContext({
90
105
  content: [{ type: 'text', text: `Pickup order: ${item}, total ${total}` }],
91
106
  structuredContent: { widget: { name: 'pickup-order', lifecycle: 'active' }, order: { item, total } },
92
107
  });
93
- }, [item, supports?.modelContext, total, updateModelContext]);
108
+ }, [item, shown, supports?.modelContext, total, updateModelContext]);
94
109
 
95
110
  async function submitOrder() {
96
- await placeOrder.callTool({ customer: shown?.customer ?? 'Guest', item });
111
+ if (!shown) return;
112
+ await placeOrder.callTool({ customer: shown.customer, item });
97
113
  if (supports?.modelContext) {
98
114
  await publishLifecycle('submitted', { surface: 'order', item, total, status: 'submitted' });
99
115
  }
@@ -102,9 +118,13 @@ export default function OrderStatus() {
102
118
  }
103
119
  }
104
120
 
121
+ if (isPending) return <Feedback status="loading">Loading the order…</Feedback>;
122
+ if (toolInfo.isError) return <Feedback status="error">Could not load the order.</Feedback>;
123
+ if (!shown) return <Feedback status="error">The order result was incomplete.</Feedback>;
124
+
105
125
  return (
106
126
  // data-llm is an inspection hint; useUpdateModelContext above is the model-visible channel.
107
- <Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown?.customer ?? 'Guest'}: ${item}, total ${total}`}>
127
+ <Frame title="Pickup order" displayMode="auto" data-llm={`Pickup order for ${shown.customer}: ${item}, total ${total}`}>
108
128
  <Flow variant="stack">
109
129
  <AsyncBoundary state={placeOrder} loading="Placing order…" error={(error) => error.message}>
110
130
  <Region title="Order" description="Choose one item for pickup.">
@@ -3,7 +3,7 @@ name: reporting-noodle-feedback
3
3
  description: "Use when a Noodle Seed bug, misleading instruction, missing capability, or concrete product improvement should be proposed to the user."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:0f404109f4845683 -->
6
+ <!-- noodle-skill version:0.44.1 hash:0f404109f4845683 -->
7
7
 
8
8
  # reporting-noodle-feedback
9
9
 
@@ -3,7 +3,7 @@ name: verifying-mcp-delivery
3
3
  description: "Use when proving a Noodle Seed MCP project works at a named compile, local, connector, App, host, deployment, or production evidence level."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:6ef6ef551e26b78e -->
6
+ <!-- noodle-skill version:0.44.1 hash:6ef6ef551e26b78e -->
7
7
 
8
8
  # verifying-mcp-delivery
9
9
 
@@ -3,7 +3,7 @@ name: noodle-seed
3
3
  description: "Use when building, validating, testing, deploying, or operating a local or hosted Noodle Seed MCP server or app authored in TypeScript with the noodle CLI."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:cd6ca0d915e6acb9 -->
6
+ <!-- noodle-skill version:0.44.1 hash:cd6ca0d915e6acb9 -->
7
7
 
8
8
  # Noodle Seed
9
9
 
@@ -3,7 +3,7 @@ name: authoring-mcp-servers
3
3
  description: "Use when creating or extending a headless Noodle Seed MCP server, tool, resource, prompt, or typed model-facing capability."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:0b2fd8c7e43fc69f -->
6
+ <!-- noodle-skill version:0.44.1 hash:0b2fd8c7e43fc69f -->
7
7
 
8
8
  # authoring-mcp-servers
9
9
 
@@ -3,7 +3,7 @@ name: building-mcp-apps
3
3
  description: "Use when a Noodle Seed MCP App, widget, interactive card, visual interaction, or host-visible UI is the primary requested outcome."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:f7fa54992c8d7692 -->
6
+ <!-- noodle-skill version:0.44.1 hash:f7fa54992c8d7692 -->
7
7
 
8
8
  # building-mcp-apps
9
9
 
@@ -3,7 +3,7 @@ name: connecting-apis-to-mcp
3
3
  description: "Use when credentials, an API URL, an OpenAPI document, or an observed response must become real Noodle Seed MCP behavior."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.44.0 hash:1e86b8704f407bd3 -->
6
+ <!-- noodle-skill version:0.44.1 hash:1e86b8704f407bd3 -->
7
7
 
8
8
  # connecting-apis-to-mcp
9
9