Embed an assistant in your SaaS
Deploy a Noodle Seed assistant, exchange your existing signed-in user on the backend, and mount the published browser SDK without exposing credentials.
The embedded assistant puts the same tools from your Noodle Seed server inside your authenticated web application. Your application keeps its existing identity system. Its backend exchanges the verified user for a short-lived assistant session; the browser never receives a model key or assistant client secret.
1. Declare branding, presentation, origins, and model references
Add assistant: embeddedAssistant(...) to the same server.ts that owns your tools. This complete example
keeps identity and colors in top-level branding, separate from the assistant's semantic presentation:
import {
embeddedAssistant,
openAICompatible,
secret,
server,
tool,
variable,
z,
} from "@noodleseed/one";
export default server(
"acme_support",
{
title: "Acme Support",
version: "1.0.0",
branding: {
name: "Acme Assistant",
accent: "#5B4CF0",
surface: "#FFFFFF",
surfaceDark: "#15131A",
mark: { uri: "https://assets.example.com/acme-mark.svg", alt: "Acme" },
colorScheme: "auto",
},
assistant: embeddedAssistant({
model: openAICompatible({
baseUrl: variable("ASSISTANT_MODEL_BASE_URL"),
model: variable("ASSISTANT_MODEL"),
apiKey: secret("ASSISTANT_MODEL_API_KEY"),
}),
allowedOrigins: ["http://localhost:3000", "https://app.example.com"],
layout: {
mode: "floating",
position: "bottom-right",
panelWidth: 520,
panelMinHeight: 540,
panelMaxHeight: 740,
edgeOffset: 24,
},
behavior: { showTimestamps: true },
labels: {
welcomeHeading: "How can Acme help?",
welcomeMessage: "Fast answers from the tools your team already uses.",
composerPlaceholder: "Message Acme Support…",
sessionReady: "Acme support is online",
},
presentation: {
panel: {
surface: "solid",
elevation: "dramatic",
border: "strong",
radius: 20,
},
launcher: {
icon: "chat",
size: "lg",
status: "session",
effect: "pulse",
},
header: {
mark: "status",
badge: { text: "Support online", tone: "success", indicator: true },
},
composer: {
leadingIcon: "brand-mark",
sendIcon: "paper-plane",
shape: "rounded",
},
messages: { userStyle: "accent", assistantStyle: "bubble" },
},
}),
},
[
tool("support_status", {
title: "Check support status",
description: "Read the support service status.",
input: z.object({}),
output: z.object({ status: z.string() }),
fulfil: () => ({ status: "operational" }),
}),
],
);Origins are exact. Do not include a path, trailing slash, or wildcard. Production origins must use
HTTPS; plain HTTP is accepted only for loopback development origins such as http://localhost:3000
or http://127.0.0.1:4002, so you can exercise the real chat against your local dev server before
you have HTTPS. noodle dev runs the MCP project; it does not serve the embedding web application.
Presentation is semantic and bounded
presentation selects renderer-owned primitives for five regions. The Atlas-style product treatment in
the complete example is the supported ceiling:
panel: solid/glass surface, soft/dramatic elevation, subtle/strong border, and optional 0-64 pixel radius.launcher: built-in brand-mark/chat/none icon, medium/large size, optional session status, and none/pulse effect.header: built-in none/brand-mark/status mark and an optional semantic-tone status badge.composer: built-in leading/send icons and rounded/pill shape.messages: bubble/accent user treatment and plain/bubble assistant treatment.
The top-level branding option is the portable deployment source for the customer name, colors, themed
logo/mark/avatar assets, typography, density, and color scheme. Semantic tones and accent treatments reuse
that palette; presentation.panel.radius is only a bounded panel-specific geometry override.
presentation has no raw HTML, CSS, inline SVG, class-name, or callback field; markup-looking strings stay
text. An HTTPS or packaged SVG referenced as a branding asset is allowed because it is not injected as
inline markup.
Omit presentation to keep the rounded Halo-inspired default: frosted glass panel with a 24px radius, soft
elevation, subtle border/motion, medium brand-mark launcher without status or effect, undecorated header,
pill composer and action controls, and rounded user bubbles with plain assistant messages at comfortable
width. A partial presentation overrides only the supplied fields.
Validate before using hosted services:
noodle validate --json
noodle check --target embedded-assistant --json2. Configure the model and deploy
Model configuration belongs to the Noodle deployment, not your web application's environment. Start with the canonical deploy:
noodle deploy --org <org> --app <app> --env <env>Deploy preflights the complete target before upload. In an interactive terminal it collects all missing
model variables and secrets, then continues. In a non-interactive run it reports every missing name and safe
noodle variables set ... --from-env / noodle secrets set ... --from-env action; run those actions and
repeat the same deploy command. Values never appear in the preflight report or resume state.
Local MCP authoring does not require an account. The external browser embed does require an active assistant-enabled deployment because its backend client is deployment-bound.
Access modes and customer auth
The embedded assistant does not require a particular --access mode: session exchange is
authenticated by your backend client credentials, independent of who may call the MCP endpoint. Add
--access customers only when verified end customers should also reach the MCP endpoint directly.
That mode requires server.auth in server.ts, and noodle deploy now enforces it before
contacting the service:
import { customerAuth } from "@noodleseed/one";
// in server options:
auth: customerAuth.federatedOidc({
issuers: [
{ issuer: "https://id.example.com", audience: "https://api.example.com" },
],
});
// or a built-in adapter such as customerAuth.firebase({ projectId, apiKey })noodle check --target embedded-assistant reports whether the manifest is ready for a
customers-access deploy.
The assistant does not select direct MCP access or protected-resource discovery. If the metadata advertises an unexpected authorization server, inspect the exact active deployment before changing auth; follow Diagnose an unexpected authorization server.
3. Create the backend client
After deployment:
noodle assistant clients create --name web --org <org> --app <app> --env <env>The CLI saves { clientId, clientSecret } to a mode-0600 file and prints only its path. Move those values
to your backend secret manager without printing or committing them.
Your web backend needs only:
NOODLE_SERVICE_URL
NOODLE_ASSISTANT_CLIENT_ID
NOODLE_ASSISTANT_CLIENT_SECRETNever expose the secret through a browser-prefixed environment variable.
Before the demo or launch, validate the complete boundary from the customer backend environment:
noodle assistant doctor --origin "$PUBLIC_APP_ORIGIN" --org <org> --app <app> --env <env>The doctor verifies the active assistant deployment, backend client credential, exact allowlisted origin,
and delegated credential exchanges that do not require an application-specific customer route, without
invoking a business tool. It reads the client secret from NOODLE_ASSISTANT_CLIENT_SECRET or the saved
mode-0600 client file and never prints it. Pass --user-id <real-test-user> when a downstream exchange
requires an existing application user. The doctor cannot choose a customer route; after minting a routed
session, invoke one representative safe read to verify the route-bound exchange and connector together.
4. Exchange your existing user
Install @noodleseed/assistant with the package manager already used by your web application. Create an
authenticated, same-origin backend route:
import { createAssistantSession } from "@noodleseed/assistant/server";
export async function POST(request: Request) {
const user = await requireCurrentUser(request);
const { context } = await request.json();
const session = await createAssistantSession({
serviceUrl: process.env.NOODLE_SERVICE_URL!,
clientId: process.env.NOODLE_ASSISTANT_CLIENT_ID!,
clientSecret: process.env.NOODLE_ASSISTANT_CLIENT_SECRET!,
origin: process.env.PUBLIC_APP_ORIGIN!,
user: {
id: user.id,
email: user.email,
roles: user.roles,
scopes: user.scopes,
},
context,
// Saved, backend-verified choices outrank browser presentation hints.
preferences: { locale: user.locale, timeZone: user.timeZone },
});
return Response.json(session);
}Backend-verified roles and OAuth-style scopes govern the same per-tool authorization rules as verified MCP
bearer claims.
Source origin from trusted server configuration, or compare the request origin against the exact allowlist
before exchange. Page context is untrusted model context, not authorization input. Forward the helper result
unchanged.
serviceUrl is the Noodle Seed control plane base URL (the value noodle assistant clients create
prints, also stored as serviceUrl in deployment.json). It is not your deployment's MCP endpoint
URL; the deployment url ends in /v1/mcp and rejects session exchange.
Route customer endpoints from the backend
If a connector uses customerEndpoint("customer_api", ...), resolve that user's API base URL from
server-owned tenancy data and bind it during the same exchange:
const user = await requireCurrentUser(request);
const account = await requireAccountMembership(user.id);
const session = await createAssistantSession({
serviceUrl,
clientId,
clientSecret,
origin,
user: { id: user.id, email: user.email },
routing: {
endpoints: {
// The key must match the customerEndpoint name in server.ts.
customer_api: account.clusterApiBaseUrl,
},
},
});The browser does not send routing. Derive it only after authenticating the user and checking their
membership in the selected account or cluster. Do not copy a route from page context, a request header,
session claims, tool arguments, or model output. Noodle accepts only endpoint names declared by the active
artifact, canonicalizes each HTTPS URL against its customerEndpoint policy, and stores the result only in
the private short-lived assistant session. It never appears in the session response or public caller
identity. The route is immutable for that session; if the user's account or cluster changes, mint a new
assistant session.
Routing is optional. If a declared endpoint is omitted, only tools that need it fail closed with
connector_route_unavailable; static tools continue to work. Confirmed routed actions also bind a URL-blind
route fingerprint at proposal time and reject a missing or changed route before connector egress when the
user accepts. If the backend explicitly supplies an unknown endpoint name or a malformed or policy-disallowed
URL, session exchange rejects it with 400 { "error": "invalid assistant routing" } and does not reflect
the URL.
Using a static Vue frontend with Django or another non-Node backend? Follow Django and Vue for the framework-neutral HTTP exchange, same-origin production routing, CSRF boundary, and Vite configuration.
The session response
The exchange returns the versioned session contract that every published widget consumes:
{
"token": "<short-lived session token>",
"expiresAt": "2026-07-13T12:34:56.000Z",
"endpoints": {
"turns": "https://<service>/v1/assistant/turns",
"toolConfirmations": "https://<service>/v1/assistant/tool-confirmations",
"interactions": "https://<service>/v1/assistant/interactions",
"apps": "https://<service>/v1/assistant/apps",
"sandbox": "https://<service>/v1/assistant/sandbox"
},
"configuration": { "branding": {}, "assistant": {} }
}token, expiresAt, and endpoints are always present; configuration optionally carries the resolved
top-level branding and assistant-only UI data, and the widget renders Halo defaults without it. Forward the
body unchanged and let the widget read it; do not rebuild or filter it.
Verified session context (identity and claims)
Pass the signed-in identity and any verified facts your backend owns at exchange time, and declare
what the assistant may receive in server.ts:
// backend route
const session = await createAssistantSession({
serviceUrl,
clientId,
clientSecret,
origin,
user: { id: user.id, email: user.email, name: user.name },
claims: { accountTier: account.tier, region: account.region },
});// server.ts
assistant: embeddedAssistant({
model,
allowedOrigins,
sessionClaims: {
accountTier: { exposeToModel: true }, // the assistant may use it directly
region: {}, // tools only
},
});Tools read user.subject, user.name, user.email, user.locale, user.timeZone, and
user.claims.<key>; the assistant automatically knows the
signed-in user's name and any exposeToModel claims, so it greets the actual user. Undeclared claims
are dropped at exchange. Keep untrusted page state in context; verified facts belong in claims.
Time, locale, and ambient application facts
The service adds a server-authoritative instant and user-local date/time to every assistant turn. Locale and IANA time zone resolve in this order:
- backend-verified
preferencesfrom session exchange; - fresh per-turn browser
clientContexthints; server.context.defaults;- platform defaults (
en-USandUTC).
Backend-verified preferences are also exposed to fulfilments and ambient providers as user.locale and
user.timeZone. This gives embedded sessions and authenticated MCP callers one application-level place to
attach a user's durable preferences; a browser hint remains per-turn presentation context and is never
promoted into verified identity.
The ambient provider above is recorded as declarative fulfilment, may call read-only connector operations
only, and has a validated output schema. The runtime resolves it once for the invocation. Tools read
context.temporal, context.ambient, and context.ambientStatus; the assistant sees the same data. To give
every host model-visible application context, mark one normal zero-input tool with contextProvider: true.
Claude, ChatGPT, and other MCP hosts can call it normally; the embedded assistant preloads it once per turn.
Core-v1 manifests retain the legacy server.context-activated noodle_context adapter, but canonical
TypeScript authoring emits Core v2 and does not create or reserve that tool.
Ambient output is deliberately compact: serialized JSON is limited to 16 KiB, nesting depth 8, and 128 entries per object or array. Credential-shaped keys are rejected. Put large documents in resources and keep authorization decisions in policy rather than treating ambient context as a data dump.
Ask for structured missing input
Use ctx.elicit when a tool needs one specific value before it can continue:
tool("prepare_time_off", {
description: "Resolve a time-off request before proposing the write.",
input: z.object({ start: z.string(), end: z.string() }),
output: z.object({ start: z.string(), end: z.string(), teamId: z.string() }),
fulfil: ({ input, elicit }) => {
const answer = elicit({
id: "choose_team",
message: "Which team should receive this request?",
input: z.object({ teamId: z.string().describe("Team") }),
});
return { start: input.start, end: input.end, teamId: answer.teamId };
},
});The input must be a flat, non-credential form of primitive fields, choices, or supported date/contact
formats. Noodle renders it as an input_requested form in the embedded assistant and maps it to standard MCP
form elicitation on bidirectional MCP transports. On stateless hosts, a linked MCP App presents the same
business-user form and resumes through standard tools/call; without Apps, the model receives the exact
schema and can collect and retry those fields conversationally. The adapter safely replays only the
compiler-guaranteed operation-free input prefix; runtime continuation and environment state remain private.
Accept validates the structured answer and resumes after the
step without rerunning completed steps; decline/cancel stop the flow. An invalid answer returns arg_invalid
and leaves the same request pending for correction. Every interactive flow must collect all elicited input
before its first connector operation. This gathers missing data but does not approve a later write—keep
action confirmation separate. For a flow marked confirm: true, every eligible input request happens before
tool_proposed; the final card reviews the original tool input, collected answers, and sole exact connector
version/operation/resolved arguments. Accepting it is the first point at which that operation may run. If a
flow asks again, the next input_requested has a fresh id; tool_completed arrives only after the final
answer.
The same confirmation policy applies outside the embed. A capable bidirectional MCP client receives a final
standard form-elicitation request and the connector runs only after both protocol acceptance and an explicit
affirmative boolean. A transport that cannot elicit fails closed before execution. At the manifest/runtime
boundary and in TypeScript action helpers, only { confirm: true } enables the gate; omitted or false
preserves direct execution. annotations.action() still supplies standard action hints, but those hints
alone never enforce approval. A confirmable flow may contain at most one connector operation. Policy and
authorization still apply.
5. Mount the browser SDK
For React:
import { NoodleAssistant } from "@noodleseed/assistant/react";
<NoodleAssistant sessionEndpoint="/api/assistant/session" theme="auto" />;For exact application-owned colors, pass the typed light/dark appearance prop. Each major region has
surface, text, and border roles; exact values are preserved, and low contrast is reported rather than
silently corrected:
<NoodleAssistant
sessionEndpoint="/api/assistant/session"
appearance={{
light: {
panel: { surface: "#FFFFFF", text: "#101828", border: "#E4E7EC" },
composer: { surface: "#F9FAFB", text: "#101828", border: "#D0D5DD" },
confirmation: { surface: "#F8FAFC", text: "#101828", border: "#CBD5E1" },
primaryButton: { surface: "#635BFF", text: "#FFFFFF" },
},
}}
onAppearanceWarning={(warning) => reportThemeWarning(warning)}
/>Want complete control over the renderer? Follow Bring your own UI to use the same assistant backend with React components and styles you own.
For a framework-neutral application, import @noodleseed/assistant once and mount:
<noodle-assistant
session-endpoint="/api/assistant/session"
theme="auto"
></noodle-assistant>Mount this backend-exchange flow only inside the authenticated application surface. A public marketing
page is a different surface with a different bootstrap: declare publicWebsite({ origins, capabilities })
alongside (or instead of) authenticatedWebsite(...), and the browser mints its own session from a
non-secret embed id with no backend route at all. One assistant serves both; each surface has its own
origins, capability allowlist, and budget.
Outside React, use the DOM-free client directly. It keeps the token in memory and exposes React-free AI SDK
UIMessage state over the exact same advertised endpoints and interaction protocol as the built-in element,
without registering a custom element:
import { createAssistantClient } from "@noodleseed/assistant/client";
const assistant = createAssistantClient({
sessionEndpoint: "/api/assistant/session",
clientContext: () => ({
locale: navigator.language,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
}),
});
// Reporting surface state does not start a model turn. The latest update replaces the prior one.
assistant.updateModelContext({
content: [{ type: "text", text: "The time-off form is mounted." }],
structuredContent: {
widget: { name: "time-off-request-form", lifecycle: "mounted" },
},
});
let pending:
| { id: string; requestedSchema?: Readonly<Record<string, unknown>> }
| undefined;
assistant.subscribeChat((state) => {
renderUIMessageState(state);
pending = undefined;
for (const message of state.messages) {
for (const part of message.parts) {
if (part.type === "data-confirmation" && part.data.status === "pending") {
pending = { id: part.data.id };
}
if (part.type === "data-input-request" && part.data.status === "pending") {
pending = {
id: part.data.id,
requestedSchema: part.data.requestedSchema,
};
}
if (part.type === "data-view") {
renderRegisteredView(part.data.resourceUri, part.data.result);
}
}
}
});
await assistant.sendMessage("Book next Thursday and Friday off");
if (pending) {
const resolution = pending.requestedSchema
? {
action: "accept" as const,
content: await renderPortableForm(pending.requestedSchema),
}
: { action: "accept" as const };
await assistant.respond(pending.id, resolution);
}
// Either interaction also accepts { action: 'decline' } or { action: 'cancel' }.subscribeChat immediately emits a detached { messages, status, error? } snapshot and then emits as its
AI SDK UIMessage.parts change. Assistant text uses text; confirmations, structured input requests, tool
results, and linked views use data-confirmation, data-input-request, data-tool-result, and data-view.
Interaction data moves through pending, submitting, accepted, declined, or cancelled. The package
does not adopt a second chat transport: React is optional and isolated to the /react and /react/client
entries, and both renderer paths delegate orchestration to the same DOM-free client. Use the lower-level
subscribe(...) event stream only for transport or session lifecycle observations that do not belong in the
transcript.
tool_proposed.arguments is a complete, schema-aware review projection of the server-held tool input and
any elicited values. For a connector-backed tool it also identifies the exact connector version, operation,
and resolved arguments; confirmable flows contain at most that one connector operation. Sensitive/write-only
fields are redacted; if any non-sensitive field cannot be presented without truncation or omission, the
platform fails closed instead of offering an incomplete approval. Accept is bound to that server-held
action, which is re-verified before one execution attempt; a client cannot replace it in the resolution
request. Decline and cancel
resolve without executing. Normal terminal outcomes scrub private arguments and continuations immediately.
Only an accepted action still in executing retains them during a one-hour unknown-outcome recovery window;
expiry records a bounded interaction_outcome_unknown result and scrubs the payload. Without downstream
idempotency this is not an exactly-once business-effect guarantee. The client never auto-retries a decision;
to reconcile a lost response, explicitly repeat the same id and decision and the service returns its durable
stored outcome without re-execution. Messages may re-exchange and retry once after a pre-execution 401.
clientContext is evaluated for each turn and is an untrusted presentation hint, never authorization.
Optional tool_proposed.title, description, and reviewSchema fields let renderers present the same
business-readable confirmation. Author the tool title and use Zod .meta({ title, description, format })
for fields; the standard card shows Confirm and Don't proceed, formats nested values without JSON, and keeps
connector mechanics collapsed under Additional details. Programmatic renderers still support cancel.
When a completed tool has an MCP App link, view_available provides a typed ui:// identity and the same
bounded, schema-redacted public result the assistant may use. It says a view is available; it does not say
the view rendered. Map resourceUri or tool to a component registered in your own application. Do not
fetch() the ui:// URI, inject its HTML into the page, or treat widget-only metadata as renderer input.
The standard element hosts the compiled MCP App in its sandboxed Apps bridge and also dispatches the same
detail so an application may replace that presentation:
assistantElement.addEventListener("assistant-view-available", (event) => {
const { resourceUri, result } = event.detail;
renderRegisteredView(resourceUri, result);
});updateModelContext({ content, structuredContent }) is also available on <noodle-assistant>. Publish one
compact, cohesive author-selected snapshot of everything the model should currently know about the surface,
such as its lifecycle and safe form summary. Each call replaces the prior snapshot rather than merging with
it, so include every still-relevant field when the surface changes. It performs no network request by itself.
Each later sendMessage carries the latest value as untrusted per-turn data, and the service does not retain
it in conversation history.
Both client and service reject credential-shaped data, values over 16 KiB, nesting deeper than 8, and
containers over 128 entries. Keep private fields private and use backend data or policy for authorization.
Inside a React MCP App, useWidgetLifecycle(name) uses that same model-context channel. Calling the hook
automatically publishes mounted, then host cancellation and teardown publish cancelled and dismissed;
use the returned function for submitted or app-specific milestones. Each lifecycle publication is also a
complete replacement snapshot. mounted means the widget code mounted, not that the host displayed pixels.
NoodleAssistant renders a custom element and must mount client-side. In a Next.js App Router page,
mark the wrapper component with 'use client'; from a server component or the Pages Router, load it
with next/dynamic and ssr: false:
import dynamic from "next/dynamic";
const AssistantWidget = dynamic(
() =>
import("@noodleseed/assistant/react").then((mod) => mod.NoodleAssistant),
{ ssr: false },
);Host integration overrides
For each matching region or token, precedence is the typed host appearance object, then trusted host slot
content or a public --ns-assistant-* CSS custom property, then compiled server
presentation/branding, then built-in defaults. The same object may be assigned to
assistantElement.appearance; the Web Component emits assistant-appearance-warning for low contrast.
Use custom properties for application-level token integration:
noodle-assistant {
--ns-assistant-accent: var(--app-primary);
--ns-assistant-font-family: var(--app-font);
--ns-assistant-panel-width: 440px;
}The public slots are launcher-icon, header-leading, header-actions, empty-state,
composer-leading, composer-trailing, and conversation-footer. Host DOM assigned to a slot replaces the
renderer fallback for that region. This is a trusted embedding-page integration API, not a deployment
escape hatch. Do not target internal shadow-DOM classes or selectors.
Content-Security-Policy on your page
If your app sends a Content-Security-Policy header, allow the Noodle service origin in two
directives — everything else can stay as strict as you like:
connect-src https://<service>; /* session turns and event streams */
frame-src https://<service>; /* the hosted widget sandbox document */App widgets render inside a sandboxed iframe that loads a static, secret-free sandbox document from
the service origin (endpoints.sandbox). That document carries its own CSP, so your page keeps a
strict script-src (nonces, no unsafe-inline) and widgets still render. Without the frame-src
entry the widget card shows a "could not be displayed" note instead of its content.
Toolchain requirements
- Node.js 20 or newer for the backend helper (
@noodleseed/assistant/server). - The package ships both ES modules and CommonJS with full export conditions, so Next.js, webpack,
Vite, and plain Node all resolve it without
transpilePackages, aliases, or type shims. - TypeScript
moduleResolutionbundlerornode16is recommended; classicnoderesolution also works for the/client,/react, and/serversubpaths.
6. Preflight the production host
Run the check-only embed mode from the web application before its production build:
noodle assistant embed --check --jsonIt reports the canonical host environment names, missing names, statically detectable
connect-src/frame-src readiness, and recommended post-deploy probes. It never returns environment
values and does not scaffold or modify files. Add application-owned backend requirements, such as the
customer side of a delegated token exchange, with a repeatable name:
noodle assistant embed --check --json --require-env EXAMPLE_DELEG_CLIENT_SECRETIf a static CSP is present, both directives must reference the NOODLE_SERVICE_URL origin. A dynamic CSP
that cannot be proven is reported as unverified; verify its generated production header before merge.
No common static CSP source is reported as not-detected; the post-deploy browser probe still verifies
headers injected by hosting infrastructure.
For each deployment environment:
- Provision the backend secret manager.
- Map every required name through the CI environment and any secret allowlist or secrets file.
- Regenerate framework-owned environment binding types when the host repository uses them.
- Run the repository's production-equivalent build before asset upload.
- Run the canonical deploy so its configuration preflight completes before asset upload, then run the JSON contract's post-deploy probes.
- Rotate assistant-client and delegated-exchange credentials independently and rerun the preflight.
Use synthetic or mock connector data in Devtools by default. Before Devtools Chat sends real connector data to an external model, disclose that data flow and obtain approval.
7. Brief your coding agent
If a coding agent (Claude Code, Codex, or another assistant) does the integration, install the Noodle Seed skills into the embedding web application's repository first:
npx @noodleseed/one@latest agents setup --writeThis writes an AGENTS.md context block, a CLAUDE.md counterpart, and the noodle-seed skill
tree (including the embedded-assistant runbook with a symptom-to-diagnosis troubleshooting table)
so the agent can complete this guide without reverse-engineering installed packages. Re-run the
command after CLI updates; noodle agents doctor reports staleness.
8. Verify the boundary
- Signed-out session exchange returns
401. - Browser network, DOM, and storage contain no client secret or model key.
- Local and production origins match
allowedOriginscharacter-for-character. - At both the manifest/runtime boundary and TypeScript action helpers, only
{ confirm: true }asks for confirmation; omitted orfalsepreserves direct execution. Action hints alone never enforce approval.noodle check --target embedded-assistantlists the tools that will confirm-gate. - Expired turns re-exchange once; interaction decisions never auto-retry. An explicit same-decision repeat returns the stored outcome without executing again.
- Accept/decline/cancel are single-use, and only accept releases the exact bound connector action.
- Wrong-origin requests fail closed.
- The production-equivalent host build passes after environment binding generation.
- A real browser keyboard submission completes session exchange and a tool turn, produces no unexpected console errors, and renders one linked App.
Use noodle commands --json before automating CLI flags, and rotate or revoke backend clients with
noodle assistant clients rotate|revoke when credentials change.
Build a Shopify checkout MCP App
Query a live Shopify Storefront API catalog, keep a local widget cart, create one Shopify cart at checkout, and hand the buyer to Shopify securely.
Embed an assistant with Django and Vue
Serve a static Vue assistant through a Django-owned session exchange without adding a Node.js production runtime or exposing backend credentials.