@noodleseed/agent-kit 0.84.0 → 0.85.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 (46) hide show
  1. package/manifest.json +273 -273
  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/creating-product-agent-guides/SKILL.md +1 -1
  8. package/skills/claude-code/debugging-mcp-delivery/SKILL.md +1 -1
  9. package/skills/claude-code/deploying-mcp-services/SKILL.md +1 -1
  10. package/skills/claude-code/designing-mcp-products/SKILL.md +1 -1
  11. package/skills/claude-code/embedding-mcp-assistants/SKILL.md +1 -1
  12. package/skills/claude-code/examples/acme-discovery/README.md +58 -15
  13. package/skills/claude-code/examples/acme-discovery/src/server.ts +127 -13
  14. package/skills/claude-code/examples/acme-discovery/test/server.test.ts +39 -0
  15. package/skills/claude-code/examples/food-ordering/src/server.ts +1 -0
  16. package/skills/claude-code/examples/food-ordering/test/server.test.ts +1 -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/embedded-assistant.md +9 -1
  20. package/skills/claude-code/references/sdk-surface.md +2 -2
  21. package/skills/claude-code/references/widgets-and-apps.md +1 -1
  22. package/skills/claude-code/reporting-noodle-feedback/SKILL.md +1 -1
  23. package/skills/claude-code/verifying-mcp-delivery/SKILL.md +1 -1
  24. package/skills/claude-code/wrapping-existing-applications/SKILL.md +1 -1
  25. package/skills/codex/SKILL.md +1 -1
  26. package/skills/codex/authoring-mcp-servers/SKILL.md +1 -1
  27. package/skills/codex/building-mcp-apps/SKILL.md +1 -1
  28. package/skills/codex/connecting-apis-to-mcp/SKILL.md +1 -1
  29. package/skills/codex/creating-product-agent-guides/SKILL.md +1 -1
  30. package/skills/codex/debugging-mcp-delivery/SKILL.md +1 -1
  31. package/skills/codex/deploying-mcp-services/SKILL.md +1 -1
  32. package/skills/codex/designing-mcp-products/SKILL.md +1 -1
  33. package/skills/codex/embedding-mcp-assistants/SKILL.md +1 -1
  34. package/skills/codex/examples/acme-discovery/README.md +58 -15
  35. package/skills/codex/examples/acme-discovery/src/server.ts +127 -13
  36. package/skills/codex/examples/acme-discovery/test/server.test.ts +39 -0
  37. package/skills/codex/examples/food-ordering/src/server.ts +1 -0
  38. package/skills/codex/examples/food-ordering/test/server.test.ts +1 -0
  39. package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
  40. package/skills/codex/publishing-mcp-integrations/SKILL.md +1 -1
  41. package/skills/codex/references/embedded-assistant.md +9 -1
  42. package/skills/codex/references/sdk-surface.md +2 -2
  43. package/skills/codex/references/widgets-and-apps.md +1 -1
  44. package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
  45. package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
  46. package/skills/codex/wrapping-existing-applications/SKILL.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noodleseed/agent-kit",
3
- "version": "0.84.0",
3
+ "version": "0.85.0",
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.84.0 hash:13ddce01769caae4 -->
6
+ <!-- noodle-skill version:0.85.0 hash:13ddce01769caae4 -->
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.84.0 hash:11523cb33b9473c0 -->
6
+ <!-- noodle-skill version:0.85.0 hash:11523cb33b9473c0 -->
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.84.0 hash:9fd67d4d24328e15 -->
6
+ <!-- noodle-skill version:0.85.0 hash:9fd67d4d24328e15 -->
7
7
 
8
8
  # building-mcp-apps
9
9
 
@@ -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.84.0 hash:21bbd3ec441ffd30 -->
6
+ <!-- noodle-skill version:0.85.0 hash:21bbd3ec441ffd30 -->
7
7
 
8
8
  # connecting-apis-to-mcp
9
9
 
@@ -3,7 +3,7 @@ name: creating-product-agent-guides
3
3
  description: "Use when a Noodle Seed MCP server needs a new or revised product agent guide, App Package skill, or explicit product-skill regeneration."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.84.0 hash:0fa48a82fe836cf0 -->
6
+ <!-- noodle-skill version:0.85.0 hash:0fa48a82fe836cf0 -->
7
7
 
8
8
  # creating-product-agent-guides
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.84.0 hash:aa715bae12041d7c -->
6
+ <!-- noodle-skill version:0.85.0 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.84.0 hash:93e735b7ffb45df1 -->
6
+ <!-- noodle-skill version:0.85.0 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.84.0 hash:78a6f181b61f92f1 -->
6
+ <!-- noodle-skill version:0.85.0 hash:78a6f181b61f92f1 -->
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.84.0 hash:cc54a67f21c0ecdb -->
6
+ <!-- noodle-skill version:0.85.0 hash:cc54a67f21c0ecdb -->
7
7
 
8
8
  # embedding-mcp-assistants
9
9
 
@@ -13,30 +13,73 @@ it" flow the `noodle-seed` skill's `references/experience-design.md` teaches.
13
13
 
14
14
  ## The same tools on Acme's own website
15
15
 
16
- The funnel does not only start in ChatGPT. The `assistant` block projects these same three tools onto
17
- Acme's marketing site for a visitor with **no account and no session backend**:
16
+ The funnel does not only start in ChatGPT. The `assistant` block projects these same tools onto
17
+ Acme's marketing site for a visitor with **no account and no session backend** — as a **mixed**
18
+ surface, so the visitor can also sign in mid-conversation:
18
19
 
19
20
  ```ts
20
- access: publicWebsite({
21
- origins: ['https://getaways.acme.example'],
22
- capabilities: [discoverGetaways, createHandoff, shortlistGetaway],
23
- instructions:
24
- 'Be a friendly, consultative travel guide, never pushy. Help visitors narrow a getaway before suggesting the next useful step.',
25
- }),
21
+ access: [
22
+ publicWebsite({
23
+ origins: ['https://getaways.acme.example'],
24
+ capabilities: [destinations, discoverGetaways, createHandoff, shortlistGetaway, captureLead, myTrips],
25
+ signIn: true, // my_trips reads ${user}; reaching it raises the sign-in card
26
+ instructions:
27
+ 'Be a friendly, consultative travel guide, never pushy. Help visitors narrow a getaway before suggesting the next useful step.',
28
+ }),
29
+ authenticatedWebsite({
30
+ origins: ['https://account.acme.example'],
31
+ capabilities: [destinations, discoverGetaways, createHandoff, myTrips],
32
+ instructions: 'The traveler is signed in. Help them plan from their saved trips.',
33
+ }),
34
+ ],
26
35
  ```
27
36
 
28
- There is no second tool set and no second app — one `server.ts`, projected onto another front door.
37
+ There is no second tool set and no second app — one `server.ts`, projected onto its front doors.
29
38
  The flagship selects `model: noodleManaged()`, so an enrolled hosted deployment needs no customer model
30
39
  endpoint, name, or key and its artifact remains provider-neutral. Sponsored beta enrollment is exact to the
31
40
  org/app/environment and fails closed outside that cohort; `openAICompatible(...)` remains the BYO alternative.
32
41
  The surface `instructions` add only the website-specific voice and goal; shared product truth stays in
33
- `server.instructions`. This public guidance is injected into public assistant turns, never MCP
42
+ `server.instructions`. This public guidance is injected into that surface's assistant turns, never MCP
34
43
  `initialize` or another assistant surface.
35
- `capabilities` is the entire externally reachable surface, so it stays short enough to review at a glance
36
- and closed by default: a tool added to this server later is unreachable from the website until someone
37
- lists it. A tool that needed a signed-in user could not be listed here at all (the compiler rejects it);
38
- serve those from `authenticatedWebsite({ origins })` instead, where Acme's own backend proves who the
39
- visitor is, or add `signIn: true` so visitors sign in mid-conversation through Acme own login.
44
+ `capabilities` is the entire externally reachable surface per front door, so it stays short enough to
45
+ review at a glance and closed by default: a tool added to this server later is unreachable from the
46
+ website until someone lists it.
47
+
48
+ ## Sign in mid-conversation, land in the account
49
+
50
+ `signIn: true` makes the marketing surface **mixed**: `my_trips` stays visible so the assistant can
51
+ offer it, and an anonymous visitor who reaches for it sees a branded card — *Sign in* plus, because
52
+ `labels.signUpAction` is authored, *Create free account*. Both raise `assistant-sign-in-requested`
53
+ with a single-use `signInTicket`; the detail's `intent` tells the page whether to route its login or
54
+ its registration. Acme's page signs the visitor in exactly as it already does, its backend spends the
55
+ ticket with `createAssistantSession({ ..., signInTicket })`, and the **same conversation continues**
56
+ on whichever origin the backend designates:
57
+
58
+ - landing back on `getaways.acme.example` keeps the mixed surface's projection with identity attached;
59
+ - landing on `account.acme.example` rebinds the conversation to the authenticated surface — its
60
+ capabilities and its voice — and the widget repaints the visible transcript and auto-answers the
61
+ intercepted `my_trips` ask under the new identity.
62
+
63
+ The ticket spend after account creation is identical to the one after sign-in; the service never
64
+ operates a login of its own.
65
+
66
+ ## The consultative sales gateway
67
+
68
+ When a visitor's plans firm up but they would rather not sign up, the assistant may — with explicit
69
+ confirmation — take their details and deliver them to Acme's own sink. The recipe is a composition of
70
+ existing primitives, not a platform feature:
71
+
72
+ - `capture_lead` is an ordinary tool with `annotations.action({ confirm: true })`: the confirmation
73
+ card, showing every field, is the visitor's consent moment.
74
+ - Delivery is a declarative HTTP connector whose endpoint is `variable('LEAD_SINK_URL')` and whose
75
+ credential is `secret('LEAD_SINK_TOKEN')` — the operator supplies values with
76
+ `noodle variables set` / `noodle secrets set`; the example stays credential-free and one authored
77
+ class serves any business.
78
+ - The request mapping sets `source: 'website-assistant'` itself, so Acme's sink can trust the
79
+ attribution; the model never supplies it.
80
+ - The lead rests only in Acme's own system. The platform stores no lead, and a vendor sink is just
81
+ different data: Resend/Postmark are `auth: { kind: 'apiKey', … }`, a HubSpot private app is
82
+ `auth: { kind: 'bearer', … }` — never a named vendor package.
40
83
 
41
84
  ## Design spec (write this before the code)
42
85
 
@@ -1,13 +1,17 @@
1
1
  import {
2
2
  annotations,
3
+ authenticatedWebsite,
4
+ connector,
3
5
  embeddedAssistant,
4
6
  file,
5
7
  knowledge,
6
8
  noodleManaged,
7
9
  publicWebsite,
10
+ secret,
8
11
  server,
9
12
  site,
10
13
  tool,
14
+ variable,
11
15
  z,
12
16
  } from '@noodleseed/one';
13
17
 
@@ -197,6 +201,90 @@ const shortlistGetaway = tool('shortlist_getaway', {
197
201
  }),
198
202
  });
199
203
 
204
+ // The consultative sales gateway (ADR 0214): when a visitor would rather not sign up, the assistant
205
+ // may — with explicit confirmation — take their details and deliver them to Acme's own sink. The
206
+ // recipe is a composition of existing primitives, not a platform feature: an ordinary confirm-gated
207
+ // action plus a declarative HTTP connector whose endpoint and credential are operator-managed
208
+ // (`noodle variables set LEAD_SINK_URL …`, `noodle secrets set LEAD_SINK_TOKEN …`). The payload
209
+ // rests only in Acme's own system; the platform stores no lead. A vendor sink is the same shape as
210
+ // data: Resend/Postmark take `auth: { kind: 'apiKey', … }`, a HubSpot private app takes
211
+ // `auth: { kind: 'bearer', … }` — never a named vendor package.
212
+ const leadSink = connector('lead_sink')
213
+ .version('1.0.0')
214
+ .http({
215
+ baseUrl: variable('LEAD_SINK_URL'),
216
+ allowedOrigins: ['https://acme.example'],
217
+ auth: { kind: 'bearer', secret: secret('LEAD_SINK_TOKEN') },
218
+ operations: {
219
+ submit_lead: {
220
+ type: 'action',
221
+ method: 'POST',
222
+ path: '/api/assistant-lead',
223
+ input: z.object({
224
+ name: z.string().trim().min(2).max(120),
225
+ workEmail: z.email().max(240),
226
+ company: z.string().trim().min(2).max(200),
227
+ note: z.string().max(500),
228
+ }),
229
+ output: z.object({ ok: z.boolean() }),
230
+ request: {
231
+ name: '${args.name}',
232
+ workEmail: '${args.workEmail}',
233
+ company: '${args.company}',
234
+ note: '${args.note}',
235
+ // Fixed attribution, set here rather than model-supplied: Acme's sink can trust it.
236
+ source: 'website-assistant',
237
+ },
238
+ response: { ok: '${response.ok}' },
239
+ },
240
+ },
241
+ });
242
+
243
+ const captureLead = tool('capture_lead', {
244
+ title: 'Send my details to Acme',
245
+ description:
246
+ 'Send the visitor’s contact details and trip interest to Acme Getaways so the team may follow ' +
247
+ 'up. Call only after the visitor explicitly agrees to be contacted; the confirmation card is ' +
248
+ 'their consent moment. After a confirmed success, say only that the details were sent — never ' +
249
+ 'promise response timing.',
250
+ annotations: annotations.action({ confirm: true }),
251
+ input: z.object({
252
+ name: z.string().trim().min(2).max(120).meta({ title: 'Your name' }),
253
+ workEmail: z.email().max(240).meta({ title: 'Work email' }),
254
+ company: z.string().trim().min(2).max(200).meta({ title: 'Company' }),
255
+ note: z.string().max(500).default('').meta({ title: 'What are you planning?' }),
256
+ }),
257
+ output: z.object({ ok: z.boolean() }),
258
+ fulfil: ({ input, connectors }) => {
259
+ const result = connectors.leads.submitLead({
260
+ name: input.name,
261
+ workEmail: input.workEmail,
262
+ company: input.company,
263
+ note: input.note,
264
+ });
265
+ return { ok: result.ok };
266
+ },
267
+ });
268
+
269
+ // The mixed surface's sign-in trigger (ADR 0201): reading `${user.id}` classifies this tool
270
+ // requires-identity, so an anonymous visitor who reaches for it sees the sign-in card instead of an
271
+ // error — and after signing in on Acme's account origin, the conversation continues under the
272
+ // authenticated surface below.
273
+ const myTrips = tool('my_trips', {
274
+ title: 'My saved trips',
275
+ description: 'Read the signed-in traveler’s saved trips and their booking status.',
276
+ annotations: readOnly,
277
+ input: z.object({}),
278
+ output: z.object({
279
+ traveler: z.string(),
280
+ status: z.string(),
281
+ }),
282
+ fulfil: ({ user }) => ({
283
+ traveler: user.id as string,
284
+ status: 'No trips booked yet — shortlist a getaway to start one.',
285
+ }),
286
+ });
287
+
200
288
  // Grounding beyond the catalog: two controlled files answer policy/pricing/support questions with
201
289
  // citations, and Acme's live public site is crawled on deploy and re-crawled on the declared
202
290
  // refresh cadence — no sync job, no handwritten search tool. One declaration, one generated
@@ -241,23 +329,49 @@ export default server(
241
329
  handoff: {
242
330
  allowedDomains: ['https://book.acme.example', 'https://acme.example'],
243
331
  },
244
- // The same three tools also serve Acme's own marketing site, with no second tool set and no
245
- // session backend: a visitor with no account gets the discovery carousel and the booking
246
- // handoff. `capabilities` is the whole externally reachable surface short enough to review in
247
- // one glance, and closed by default when a tool is added to the server later. The knowledge
248
- // component projects its generated search capability the same way a tool does.
332
+ use: { leads: leadSink },
333
+ // The same tools also serve Acme's own websites, with no second tool set. The marketing site is
334
+ // a **mixed** surface (`signIn: true`): a visitor with no account gets discovery, the booking
335
+ // handoff, and the confirm-gated lead capture and reaching for `my_trips` raises the sign-in
336
+ // card instead of an error, with `signUpAction` offering account creation through Acme's own
337
+ // registration. After the login redirect lands on the account origin, the same conversation
338
+ // continues under the authenticated surface's capabilities and instructions (ADR 0201).
339
+ // `capabilities` is the whole externally reachable surface per front door — short enough to
340
+ // review in one glance, and closed by default when a tool is added to the server later.
249
341
  assistant: embeddedAssistant({
250
342
  model: noodleManaged(),
251
- access: publicWebsite({
252
- origins: ['https://getaways.acme.example'],
253
- capabilities: [destinations, discoverGetaways, createHandoff, shortlistGetaway],
254
- instructions:
255
- 'Be a friendly, consultative travel guide, never pushy. Help visitors narrow a getaway before suggesting the next useful step. Ground recommendations in Acme knowledge, and clearly separate discovery from booking.',
256
- }),
343
+ access: [
344
+ publicWebsite({
345
+ origins: ['https://getaways.acme.example'],
346
+ capabilities: [
347
+ destinations,
348
+ discoverGetaways,
349
+ createHandoff,
350
+ shortlistGetaway,
351
+ captureLead,
352
+ myTrips,
353
+ ],
354
+ signIn: true,
355
+ instructions:
356
+ 'Be a friendly, consultative travel guide, never pushy. Help visitors narrow a getaway before suggesting the next useful step. Ground recommendations in Acme knowledge, and clearly separate discovery from booking. When a visitor’s plans firm up, invite them to sign in or create an account; if they would rather not, offer — once — to send their details to the Acme team instead.',
357
+ }),
358
+ authenticatedWebsite({
359
+ origins: ['https://account.acme.example'],
360
+ capabilities: [destinations, discoverGetaways, createHandoff, myTrips],
361
+ instructions:
362
+ 'The traveler is signed in. Help them plan from their saved trips, and keep booking on Acme’s own pages through the handoff.',
363
+ }),
364
+ ],
257
365
  layout: { mode: 'floating', position: 'bottom-right' },
258
- labels: { welcomeHeading: 'Where would you like to go?' },
366
+ labels: {
367
+ welcomeHeading: 'Where would you like to go?',
368
+ signInHeading: 'Continue with your Acme account',
369
+ signInBody: 'Saved trips need an account.',
370
+ signInAction: 'Sign in',
371
+ signUpAction: 'Create free account',
372
+ },
259
373
  }),
260
374
  knowledge: [destinations],
261
375
  },
262
- [discoverGetaways, createHandoff, shortlistGetaway],
376
+ [discoverGetaways, createHandoff, shortlistGetaway, captureLead, myTrips],
263
377
  );
@@ -30,6 +30,45 @@ describe('acme-discovery example', () => {
30
30
  );
31
31
  });
32
32
 
33
+ it('keeps the lead capture behind explicit confirmation and a managed customer sink', async () => {
34
+ const manifest = (await app.toManifest()) as {
35
+ tools: { name: string; annotations?: Record<string, unknown> }[];
36
+ };
37
+ const captureLead = manifest.tools.find((tool) => tool.name === 'capture_lead');
38
+ // The confirmation card is the visitor's consent moment (ADR 0214): a lead may never leave the
39
+ // conversation without it, and the sink endpoint/credential stay operator-managed data.
40
+ expect(captureLead?.annotations?.confirm).toBe(true);
41
+ const catalog = JSON.stringify(
42
+ (app as unknown as { toConnectorCatalog: () => unknown }).toConnectorCatalog(),
43
+ );
44
+ expect(catalog).toContain('${env.LEAD_SINK_URL}');
45
+ expect(catalog).toContain('LEAD_SINK_TOKEN');
46
+ // Fixed attribution set in the request mapping, never model-supplied; no named vendor host.
47
+ expect(catalog).toContain('website-assistant');
48
+ expect(catalog).not.toContain('api.resend.com');
49
+ expect(catalog).not.toContain('api.hubapi.com');
50
+ });
51
+
52
+ it('serves a mixed marketing surface and an authenticated account surface from one server', async () => {
53
+ const manifest = await app.toManifest();
54
+ const surfaces = manifest.server.assistant?.surfaces ?? [];
55
+ expect(surfaces.map((surface) => surface.mode)).toEqual(['mixed', 'authenticated']);
56
+ // The sign-in trigger is listed on the mixed surface so the assistant can offer it; the
57
+ // authenticated surface carries its own narrowed list and voice.
58
+ const capabilityNames = (surface: (typeof surfaces)[number]) =>
59
+ surface.capabilities?.map((capability) => capability.name) ?? [];
60
+ expect(capabilityNames(surfaces[0]!)).toContain('my_trips');
61
+ expect(capabilityNames(surfaces[0]!)).toContain('capture_lead');
62
+ expect(capabilityNames(surfaces[1]!)).toEqual([
63
+ 'destinations',
64
+ 'discover_getaways',
65
+ 'create_handoff',
66
+ 'my_trips',
67
+ ]);
68
+ // Authoring the sign-up label is the opt-in for the card's create-account button.
69
+ expect(manifest.server.assistant?.labels?.signUpAction).toBe('Create free account');
70
+ });
71
+
33
72
  it('declares the grounded knowledge component and its live site scope', async () => {
34
73
  const manifest = (await app.toManifest()) as { server: { knowledge?: unknown[] } };
35
74
  // One declaration: controlled files plus the live public site, compiled later into the
@@ -363,6 +363,7 @@ export default server(
363
363
  'delivery',
364
364
  'checkout',
365
365
  ],
366
+ oncePerSession: true,
366
367
  },
367
368
  input: z.object({
368
369
  query: z.string().optional(),
@@ -86,6 +86,7 @@ describe('food-ordering example', () => {
86
86
  'menu',
87
87
  'checkout',
88
88
  ]),
89
+ 'x-noodleseed-model-once-per-session': true,
89
90
  });
90
91
  expect(JSON.stringify(tools.get('sync_cart'))).toContain('revision');
91
92
  expect(tools.get('sync_cart')?.annotations?.confirm).toBe(false);
@@ -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.84.0 hash:6a9f132ddb79352e -->
6
+ <!-- noodle-skill version:0.85.0 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.84.0 hash:efffbf82007f935d -->
6
+ <!-- noodle-skill version:0.85.0 hash:efffbf82007f935d -->
7
7
 
8
8
  # publishing-mcp-integrations
9
9
 
@@ -107,11 +107,19 @@ access: publicWebsite({
107
107
  capabilities: [answerProductQuestion, requestDemo, myOrders],
108
108
  signIn: true, // `myOrders` reads ${user}; visitors sign in to reach it
109
109
  }),
110
+ labels: {
111
+ signInHeading: "Continue with your Acme account",
112
+ signInBody: "Order history needs an account.", // optional; empty hides the line
113
+ signInAction: "Sign in",
114
+ signUpAction: "Create free account", // authoring this label is the sign-up opt-in
115
+ },
110
116
  ```
111
117
 
118
+ The sign-in card renders on the same themed chrome as every proposal card and follows the server `branding`. Authoring `signUpAction` adds a second button; leaving it out renders none. Both buttons raise the same `assistant-sign-in-requested` event with the same single-use ticket — the detail adds `intent: "sign-in" | "sign-up"` so the page routes `sign-up` to its registration page instead of its login. The ticket spend after account creation is identical to the one after sign-in; the service does not care which path produced the session. Headless renderers receive the same moment as a `data-sign-in` transcript part from `subscribeChat` (it has no status and is not respondable through `client.respond` — resolution is the elevated session).
119
+
112
120
  Elevation runs through the **host application own login**, never a Noodle-operated one. The widget raises `assistant-sign-in-requested` with a single-use `signInTicket` in its detail; the page signs the visitor in as it already does, then its backend spends the ticket with `createAssistantSession({ ..., signInTicket })` from `@noodleseed/assistant/server` — the same session exchange, its own client credentials. A refused spend throws a typed `AssistantSessionExchangeError`: branch on `elevationRefusal` (`elevation_ticket_expired` re-prompt; `elevation_tenant_mismatch` alert, never retry). Possession of the ticket alone elevates nothing, and the service checks the client tenant owns that conversation. The ticket is not the server-held interaction continuation — that value never reaches browser code; this one exists to travel through the page.
113
121
 
114
- The conversation is kept server-side: same session, new token, the anonymous one dead. By default the pending request also completes itself: the service re-attempts the intercepted tool under the new principal and streams it as the elevated session first turn (one-shot; mooted if the visitor types first; confirm-gated tools stop at their confirmation card; pass `resume: false` beside the ticket to disable). Set honest expectations in UI copy the assistant **remembers** the conversation and finishes the pending request, but no transcript is replayed to the browser, so after a full-page navigation earlier messages do not repaint. Say "the assistant remembers", never "your conversation will reappear". Do not build a second identity provider or client-side resume scaffolding for this.
122
+ The conversation is kept server-side: same session, new token, the anonymous one dead. By default the pending request also completes itself: the service re-attempts the intercepted tool under the new principal and streams it as the elevated session first turn (one-shot; mooted if the visitor types first; confirm-gated tools stop at their confirmation card; pass `resume: false` beside the ticket to disable). On a backend-exchanged reattach the widget also repaints the bounded visible transcript (`endpoints.transcript`) before the resume runs only rows the panel actually showed replay, never tool internals or a spent ticket. On an older service without the endpoint the panel starts visually fresh while the model still remembers, so keep copy honest either way: "the assistant remembers". Do not build a second identity provider or client-side resume scaffolding for this.
115
123
 
116
124
  When the login lives on a different origin (marketing site + app), the flow is: the visitor signs in via full-page redirect as the site already does; the backend spends the ticket presenting the **origin the conversation will continue on** (any origin in the deployment allowlist — elevation re-pins the session there, and CORS follows); the token reaches the widget through the customer own **same-origin** session endpoint on that origin. The redirect handoff is mandatory, not stylistic: the widget calls the session endpoint with `credentials: "same-origin"`, so pointing a marketing page at a cross-origin endpoint is a guaranteed cookie-less 401. Persist the ticket across the login redirect (single-use, expires in minutes); a refused origin does not burn it.
117
125
 
@@ -74,7 +74,7 @@ Platform helper connectors are explicit subpath imports from `@noodleseed/one/pl
74
74
  ## Authoring signatures
75
75
 
76
76
  - `server(name, options, definitions)` — `options` commonly includes `title`, `version`, `instructions`, `agentGuide`, `distribution`, `branding`, `auth`, `use`, `provides`, `state`, and `handoff`; `definitions` is the array of tools/resources/prompts.
77
- - `tool(name, { description, input, output, annotations?, visibility?, modelVisibility?, 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. Use `modelVisibility.latestMessageIncludesAny` only for normalized literal explicit-intent discovery; it does not authorize execution.
77
+ - `tool(name, { description, input, output, annotations?, visibility?, modelVisibility?, 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. Use `modelVisibility.latestMessageIncludesAny` only for normalized literal explicit-intent discovery; `oncePerSession` and `requiredWhenVisible` add deterministic presentation controls, never authorization or idempotency.
78
78
  - Keep tool input names application-owned and meaningful; `__noodleIntent` is reserved for an optional serve-time operator analytics adapter and never reaches `fulfil`.
79
79
  - `resource(name, { uri, description?, mimeType?, fulfil })` and `prompt(name, { description?, arguments?, fulfil })` expose MCP resources/prompts.
80
80
  - View metadata (`viewTitle`, `viewDescription`, `csp`, `domain`, `permissions`) belongs on the tool that renders it; `asset("./path")` packages local files.
@@ -146,7 +146,7 @@ prompt('summarize_ticket', {
146
146
 
147
147
  ### Non-trivial tool: ctx connectors, annotations, visibility, async
148
148
 
149
- `ctx` is `{ input, user, connectors }`. Bind connectors with `use` on the server, then call one inside `fulfil` to record a step. `annotations.readOnly()` declares a closed-world safe read. TypeScript action helpers enforce confirmation only with `{ confirm: true }`; omitted or `false` executes directly, and action/destructive/open-world hints alone never enable the gate. For stateless hosts that cannot present Noodle confirmation, set `interactions: { confirmationFallback: 'host' }` in the `server` options to explicitly trust native host write approval; omission remains fail-closed and the fallback never supplies missing `ctx.elicit` input. `visibility` defaults to `['model', 'app']` — set `['app']` to hide a helper from the model. For a narrow explicit-intent tool, `modelVisibility: { latestMessageIncludesAny: [...] }` deterministically limits model discovery to a latest user message containing one of the normalized literal phrases; it is not authorization. `fulfil` may be `async` (the compiler awaits it while recording).
149
+ `ctx` is `{ input, user, connectors }`. Bind connectors with `use` on the server, then call one inside `fulfil` to record a step. `annotations.readOnly()` declares a closed-world safe read. TypeScript action helpers enforce confirmation only with `{ confirm: true }`; omitted or `false` executes directly, and action/destructive/open-world hints alone never enable the gate. For stateless hosts that cannot present Noodle confirmation, set `interactions: { confirmationFallback: 'host' }` in the `server` options to explicitly trust native host write approval; omission remains fail-closed and the fallback never supplies missing `ctx.elicit` input. `visibility` defaults to `['model', 'app']` — set `['app']` to hide a helper from the model. For a narrow explicit-intent tool, `modelVisibility: { latestMessageIncludesAny: [...] }` deterministically limits model discovery to a latest user message containing one normalized literal phrase. Add `oncePerSession: true` to prevent another successful model-selected use in that conversation, and `requiredWhenVisible: true` only when the matching tool must be called before normal discovery resumes. These are presentation controls, not authorization or idempotency. `fulfil` may be `async` (the compiler awaits it while recording).
150
150
 
151
151
  ```ts
152
152
  import { annotations, connector, server, tool, z } from '@noodleseed/one';
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## Tools and views
14
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. This is relevance filtering, never authorization. A `view` is `{ component: "name", entry: "./views/name.tsx" }` — a React component the compiler bundles at validate/deploy time.
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
16
 
17
17
  ## Noodle Design default
18
18
 
@@ -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.84.0 hash:0f404109f4845683 -->
6
+ <!-- noodle-skill version:0.85.0 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.84.0 hash:6ef6ef551e26b78e -->
6
+ <!-- noodle-skill version:0.85.0 hash:6ef6ef551e26b78e -->
7
7
 
8
8
  # verifying-mcp-delivery
9
9
 
@@ -3,7 +3,7 @@ name: wrapping-existing-applications
3
3
  description: "Use when an existing application has no stable usable API and needs a read-only, identity-first Noodle Seed integration plan before implementation."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.84.0 hash:eccc3c158dcafba8 -->
6
+ <!-- noodle-skill version:0.85.0 hash:eccc3c158dcafba8 -->
7
7
 
8
8
  # wrapping-existing-applications
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.84.0 hash:13ddce01769caae4 -->
6
+ <!-- noodle-skill version:0.85.0 hash:13ddce01769caae4 -->
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.84.0 hash:11523cb33b9473c0 -->
6
+ <!-- noodle-skill version:0.85.0 hash:11523cb33b9473c0 -->
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.84.0 hash:9fd67d4d24328e15 -->
6
+ <!-- noodle-skill version:0.85.0 hash:9fd67d4d24328e15 -->
7
7
 
8
8
  # building-mcp-apps
9
9
 
@@ -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.84.0 hash:21bbd3ec441ffd30 -->
6
+ <!-- noodle-skill version:0.85.0 hash:21bbd3ec441ffd30 -->
7
7
 
8
8
  # connecting-apis-to-mcp
9
9
 
@@ -3,7 +3,7 @@ name: creating-product-agent-guides
3
3
  description: "Use when a Noodle Seed MCP server needs a new or revised product agent guide, App Package skill, or explicit product-skill regeneration."
4
4
  ---
5
5
 
6
- <!-- noodle-skill version:0.84.0 hash:0fa48a82fe836cf0 -->
6
+ <!-- noodle-skill version:0.85.0 hash:0fa48a82fe836cf0 -->
7
7
 
8
8
  # creating-product-agent-guides
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.84.0 hash:aa715bae12041d7c -->
6
+ <!-- noodle-skill version:0.85.0 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.84.0 hash:93e735b7ffb45df1 -->
6
+ <!-- noodle-skill version:0.85.0 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.84.0 hash:78a6f181b61f92f1 -->
6
+ <!-- noodle-skill version:0.85.0 hash:78a6f181b61f92f1 -->
7
7
 
8
8
  # designing-mcp-products
9
9