@noodleseed/agent-kit 0.21.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +61 -11
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +1 -1
- package/skills/claude-code/examples/customer-auth/README.md +237 -0
- package/skills/claude-code/examples/customer-auth/noodle.json +4 -0
- package/skills/claude-code/examples/customer-auth/package.json +16 -0
- package/skills/claude-code/examples/customer-auth/src/server.ts +145 -0
- package/skills/claude-code/examples/customer-auth/test/server.test.ts +25 -0
- package/skills/claude-code/examples/hello/src/server.ts +3 -1
- package/skills/claude-code/references/authoring-workflow.md +72 -2
- package/skills/claude-code/references/embedded-assistant.md +54 -2
- package/skills/claude-code/references/examples.md +1 -1
- package/skills/codex/SKILL.md +1 -1
- package/skills/codex/examples/customer-auth/README.md +237 -0
- package/skills/codex/examples/customer-auth/noodle.json +4 -0
- package/skills/codex/examples/customer-auth/package.json +16 -0
- package/skills/codex/examples/customer-auth/src/server.ts +145 -0
- package/skills/codex/examples/customer-auth/test/server.test.ts +25 -0
- package/skills/codex/examples/hello/src/server.ts +3 -1
- package/skills/codex/references/authoring-workflow.md +72 -2
- package/skills/codex/references/embedded-assistant.md +54 -2
- package/skills/codex/references/examples.md +1 -1
package/manifest.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "0.
|
|
2
|
+
"packageVersion": "0.22.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": "skills/codex/SKILL.md",
|
|
6
|
-
"sha256": "
|
|
6
|
+
"sha256": "854f9f274724ac7eeb5bdeb933f83b36cbcbeb3643e0de806ecc33fce89da832",
|
|
7
7
|
"agentTarget": "codex"
|
|
8
8
|
},
|
|
9
9
|
{
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
"path": "skills/codex/references/authoring-workflow.md",
|
|
31
|
-
"sha256": "
|
|
31
|
+
"sha256": "a2389c7cdbb28a817b8e5b616b625c02fed25fc017845349892c6e1b546a71c0",
|
|
32
32
|
"agentTarget": "codex"
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
"path": "skills/codex/references/embedded-assistant.md",
|
|
36
|
-
"sha256": "
|
|
36
|
+
"sha256": "e72fbea15298b980ccd876ced45f9e0b17b472859e1f5731283d7677ed9b0f13",
|
|
37
37
|
"agentTarget": "codex"
|
|
38
38
|
},
|
|
39
39
|
{
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"path": "skills/codex/references/examples.md",
|
|
81
|
-
"sha256": "
|
|
81
|
+
"sha256": "69ab3b0e85454f3d5df5f270b760216de7b1a69803a2f081447bc27bb634c524",
|
|
82
82
|
"agentTarget": "codex"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
@@ -251,6 +251,31 @@
|
|
|
251
251
|
"sha256": "c486293ce4a0d38450f61533da17e419ca9fa490ef9577e8404f4fd700797747",
|
|
252
252
|
"agentTarget": "codex"
|
|
253
253
|
},
|
|
254
|
+
{
|
|
255
|
+
"path": "skills/codex/examples/customer-auth/README.md",
|
|
256
|
+
"sha256": "ae55e8c5e88216a3a3b1f57084f16e7af0edbf1e5b35ade7be5d5394887347ae",
|
|
257
|
+
"agentTarget": "codex"
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
"path": "skills/codex/examples/customer-auth/noodle.json",
|
|
261
|
+
"sha256": "15824af8846e8da4f9d14c28216ff95076dfc9f9964fcf127f441d44517d0eea",
|
|
262
|
+
"agentTarget": "codex"
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
"path": "skills/codex/examples/customer-auth/package.json",
|
|
266
|
+
"sha256": "bfbc1e9aa5f3c7f5449a6ade8ff78876f46ce77b7b867f26bdb556edee3171db",
|
|
267
|
+
"agentTarget": "codex"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
"path": "skills/codex/examples/customer-auth/src/server.ts",
|
|
271
|
+
"sha256": "549dd7d0ccfb2fff7b0f3ce23194145e7364f5fc337a27e2b6d50b3dd7fb264d",
|
|
272
|
+
"agentTarget": "codex"
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"path": "skills/codex/examples/customer-auth/test/server.test.ts",
|
|
276
|
+
"sha256": "97e9f00f102ea65f74dd9bca93090eb961af431a8c1285c80028dfae191476d9",
|
|
277
|
+
"agentTarget": "codex"
|
|
278
|
+
},
|
|
254
279
|
{
|
|
255
280
|
"path": "skills/codex/examples/food-ordering/README.md",
|
|
256
281
|
"sha256": "4d42dfbaf46ee8003ae84b3ef79c9d2a8b716943b85e2e9329fa875007019524",
|
|
@@ -318,7 +343,7 @@
|
|
|
318
343
|
},
|
|
319
344
|
{
|
|
320
345
|
"path": "skills/codex/examples/hello/src/server.ts",
|
|
321
|
-
"sha256": "
|
|
346
|
+
"sha256": "5b7dd35428128cfe0f506054e80b060d8a87ef157b03d2ce94d304b14df6e521",
|
|
322
347
|
"agentTarget": "codex"
|
|
323
348
|
},
|
|
324
349
|
{
|
|
@@ -353,7 +378,7 @@
|
|
|
353
378
|
},
|
|
354
379
|
{
|
|
355
380
|
"path": "skills/claude-code/SKILL.md",
|
|
356
|
-
"sha256": "
|
|
381
|
+
"sha256": "6b0569348845fc943f50fe9a4ba408034316c9f8bf4867734b41bfba494f8310",
|
|
357
382
|
"agentTarget": "claude-code"
|
|
358
383
|
},
|
|
359
384
|
{
|
|
@@ -378,12 +403,12 @@
|
|
|
378
403
|
},
|
|
379
404
|
{
|
|
380
405
|
"path": "skills/claude-code/references/authoring-workflow.md",
|
|
381
|
-
"sha256": "
|
|
406
|
+
"sha256": "a2389c7cdbb28a817b8e5b616b625c02fed25fc017845349892c6e1b546a71c0",
|
|
382
407
|
"agentTarget": "claude-code"
|
|
383
408
|
},
|
|
384
409
|
{
|
|
385
410
|
"path": "skills/claude-code/references/embedded-assistant.md",
|
|
386
|
-
"sha256": "
|
|
411
|
+
"sha256": "e72fbea15298b980ccd876ced45f9e0b17b472859e1f5731283d7677ed9b0f13",
|
|
387
412
|
"agentTarget": "claude-code"
|
|
388
413
|
},
|
|
389
414
|
{
|
|
@@ -428,7 +453,7 @@
|
|
|
428
453
|
},
|
|
429
454
|
{
|
|
430
455
|
"path": "skills/claude-code/references/examples.md",
|
|
431
|
-
"sha256": "
|
|
456
|
+
"sha256": "69ab3b0e85454f3d5df5f270b760216de7b1a69803a2f081447bc27bb634c524",
|
|
432
457
|
"agentTarget": "claude-code"
|
|
433
458
|
},
|
|
434
459
|
{
|
|
@@ -601,6 +626,31 @@
|
|
|
601
626
|
"sha256": "c486293ce4a0d38450f61533da17e419ca9fa490ef9577e8404f4fd700797747",
|
|
602
627
|
"agentTarget": "claude-code"
|
|
603
628
|
},
|
|
629
|
+
{
|
|
630
|
+
"path": "skills/claude-code/examples/customer-auth/README.md",
|
|
631
|
+
"sha256": "ae55e8c5e88216a3a3b1f57084f16e7af0edbf1e5b35ade7be5d5394887347ae",
|
|
632
|
+
"agentTarget": "claude-code"
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
"path": "skills/claude-code/examples/customer-auth/noodle.json",
|
|
636
|
+
"sha256": "15824af8846e8da4f9d14c28216ff95076dfc9f9964fcf127f441d44517d0eea",
|
|
637
|
+
"agentTarget": "claude-code"
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
"path": "skills/claude-code/examples/customer-auth/package.json",
|
|
641
|
+
"sha256": "bfbc1e9aa5f3c7f5449a6ade8ff78876f46ce77b7b867f26bdb556edee3171db",
|
|
642
|
+
"agentTarget": "claude-code"
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
"path": "skills/claude-code/examples/customer-auth/src/server.ts",
|
|
646
|
+
"sha256": "549dd7d0ccfb2fff7b0f3ce23194145e7364f5fc337a27e2b6d50b3dd7fb264d",
|
|
647
|
+
"agentTarget": "claude-code"
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
"path": "skills/claude-code/examples/customer-auth/test/server.test.ts",
|
|
651
|
+
"sha256": "97e9f00f102ea65f74dd9bca93090eb961af431a8c1285c80028dfae191476d9",
|
|
652
|
+
"agentTarget": "claude-code"
|
|
653
|
+
},
|
|
604
654
|
{
|
|
605
655
|
"path": "skills/claude-code/examples/food-ordering/README.md",
|
|
606
656
|
"sha256": "4d42dfbaf46ee8003ae84b3ef79c9d2a8b716943b85e2e9329fa875007019524",
|
|
@@ -668,7 +718,7 @@
|
|
|
668
718
|
},
|
|
669
719
|
{
|
|
670
720
|
"path": "skills/claude-code/examples/hello/src/server.ts",
|
|
671
|
-
"sha256": "
|
|
721
|
+
"sha256": "5b7dd35428128cfe0f506054e80b060d8a87ef157b03d2ce94d304b14df6e521",
|
|
672
722
|
"agentTarget": "claude-code"
|
|
673
723
|
},
|
|
674
724
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noodleseed/agent-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.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",
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# Customer Auth - NoodleSeed.com Firebase customer identity
|
|
2
|
+
|
|
3
|
+
This curated example owns the customer/end-user authentication with Firebase bridge capability slot. It is
|
|
4
|
+
the NoodleSeed.com dogfood app for proving that a SaaS app can protect an MCP endpoint with its own customer
|
|
5
|
+
identity provider while still using the generic Noodle Seed authoring API.
|
|
6
|
+
|
|
7
|
+
It also owns the embedded-assistant showcase: the same authenticated MCP surface can be dropped into the
|
|
8
|
+
SaaS web application as a fully customer-branded assistant with independent light and dark themes. The
|
|
9
|
+
assistant loads the active deployment's instructions and model-visible tools rather than installing a stale
|
|
10
|
+
second skill bundle. Interactive MCP Apps widgets continue to render in supported external hosts; the
|
|
11
|
+
initial embedded surface renders text and native tool confirmations.
|
|
12
|
+
|
|
13
|
+
The public developer entrypoint is [`src/server.ts`](src/server.ts). It declares `customerAuth.firebase(...)` with
|
|
14
|
+
the NoodleSeed.com Firebase project and Firebase Web App public config. It exposes a deliberately small MCP
|
|
15
|
+
surface for org discovery:
|
|
16
|
+
|
|
17
|
+
- `list_my_organizations` lists the NoodleSeed.com organizations the signed-in customer belongs to (no
|
|
18
|
+
arguments — the org set comes from the verified customer session).
|
|
19
|
+
- `list_org_apps` lists apps for one of those organizations through the dev app API.
|
|
20
|
+
|
|
21
|
+
The two tools chain: `list_my_organizations` surfaces the `org_id`s the customer can act on, and
|
|
22
|
+
`list_org_apps` takes one of those `org_id`s. There is no NoodleSeed-specific SDK helper. The downstream API
|
|
23
|
+
is an ordinary authored HTTP connector.
|
|
24
|
+
|
|
25
|
+
During MCP OAuth login, Noodle Cloud hosts the Firebase bridge page at
|
|
26
|
+
`https://cloud.noodleseed.dev/oauth/customer/firebase/authorize`. The customer app does not add an
|
|
27
|
+
authorization route. The SaaS operator only configures Firebase Auth to allow the Noodle Cloud origin, and
|
|
28
|
+
Noodle Cloud signs the customer in with Firebase before posting the Firebase ID token back to its own bridge
|
|
29
|
+
callback.
|
|
30
|
+
|
|
31
|
+
## How delegated customer credentials are used
|
|
32
|
+
|
|
33
|
+
The example has two declarations that work together:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
auth: customerAuth.firebase({
|
|
37
|
+
projectId: variable('FIREBASE_PROJECT_ID'),
|
|
38
|
+
apiKey: variable('FIREBASE_WEB_API_KEY'),
|
|
39
|
+
authDomain: variable('FIREBASE_AUTH_DOMAIN'),
|
|
40
|
+
user: {
|
|
41
|
+
id: 'sub',
|
|
42
|
+
email: 'email',
|
|
43
|
+
name: 'name',
|
|
44
|
+
tenant: 'firebase.tenant',
|
|
45
|
+
orgs: 'claims.orgs',
|
|
46
|
+
roles: 'claims.roles',
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
That protects the MCP endpoint with the SaaS customer's Firebase identity. The connector then opts into
|
|
52
|
+
delegated customer credentials:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
auth: {
|
|
56
|
+
kind: 'delegatedSessionCookie',
|
|
57
|
+
provider: 'firebase',
|
|
58
|
+
sessionUrl: `${noodleseedApiOrigin}/api/auth/session`,
|
|
59
|
+
tokenField: 'idToken',
|
|
60
|
+
},
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Tool code calls the connector normally:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
fulfil({ input, connectors }) {
|
|
67
|
+
const apps = connectors.app_api.listOrgApps({
|
|
68
|
+
org_id: input.org_id,
|
|
69
|
+
skip: input.skip,
|
|
70
|
+
limit: input.limit,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return { result: apps.result };
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
At runtime, Noodle Seed verifies the Firebase customer during MCP OAuth, stores that customer's delegated
|
|
78
|
+
Firebase refresh token in the credential broker, and refreshes a short-lived Firebase ID token only when a
|
|
79
|
+
connector-backed tool calls the NoodleSeed.com API. For this app API, the broker exchanges that ID token at
|
|
80
|
+
the existing Next.js `/api/auth/session` route and sends the resulting session cookie to the API. The MCP
|
|
81
|
+
access token remains a Noodle-issued resource-bound token and is never sent to the downstream API.
|
|
82
|
+
|
|
83
|
+
## Delegated downstream auth for your own API (token exchange)
|
|
84
|
+
|
|
85
|
+
The Firebase path above only works for Firebase-session downstreams. When the downstream is **your own
|
|
86
|
+
API** with its own token issuance, use `delegatedTokenExchange` instead
|
|
87
|
+
([ADR 0152](../../docs/decisions/0152-delegated-token-exchange-connector-auth.md)): the platform signs a
|
|
88
|
+
short-lived, JWKS-verifiable assertion of the signed-in user and exchanges it (RFC 8693) at a token
|
|
89
|
+
endpoint you implement, which mints your own user-scoped token — so your API enforces its own per-user
|
|
90
|
+
authorization on every call. It works for identities from `customerAuth.bridge(...)` and from embedded
|
|
91
|
+
assistant sessions, with no per-user OAuth enrollment.
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
auth: {
|
|
95
|
+
kind: 'delegatedTokenExchange',
|
|
96
|
+
tokenUrl: 'https://app.example.com/api/assistant/oauth/token', // origin must be in allowedOrigins
|
|
97
|
+
clientId: variable('EXAMPLE_DELEG_CLIENT_ID'),
|
|
98
|
+
clientSecret: secret('EXAMPLE_DELEG_CLIENT_SECRET'),
|
|
99
|
+
scopes: ['time_off'],
|
|
100
|
+
},
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Your endpoint authenticates the broker's client credential, verifies the `subject_token` JWT against the
|
|
104
|
+
platform issuer JWKS (claims include the verified `sub`, `email`, `name`, declared session `claims`,
|
|
105
|
+
`tenant`, and `deployment`), mints a short-lived user-scoped token, and returns the standard
|
|
106
|
+
`{ access_token, token_type, expires_in }` response. The exact wire contract and a copyable endpoint
|
|
107
|
+
implementation live in [docs/spec/connectors.md](../../docs/spec/connectors.md) and the Agent Kit
|
|
108
|
+
authoring-workflow reference ("Delegated downstream auth"). `noodle auth doctor` reports each declared
|
|
109
|
+
exchange endpoint.
|
|
110
|
+
|
|
111
|
+
## Validate
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
noodle auth doctor examples/customer-auth/src/server.ts
|
|
115
|
+
noodle validate examples/customer-auth/src/server.ts
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Run locally
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
noodle dev examples/customer-auth/src/server.ts --app noodleseed-customer-auth
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
The embedded assistant uses a customer-supplied OpenAI Chat Completions-compatible endpoint. Configure its
|
|
127
|
+
managed values at the Noodle deployment environment; none of these values belongs in the customer web
|
|
128
|
+
application environment, and the API key never reaches the browser:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
noodle variables set ASSISTANT_MODEL_BASE_URL https://model.example.com/v1 --scope env
|
|
132
|
+
noodle variables set ASSISTANT_MODEL your-model --scope env
|
|
133
|
+
noodle secrets set ASSISTANT_MODEL_API_KEY --scope env
|
|
134
|
+
noodle variables set FIREBASE_PROJECT_ID your-firebase-project --scope env
|
|
135
|
+
noodle variables set FIREBASE_WEB_API_KEY your-firebase-web-api-key --scope env
|
|
136
|
+
noodle variables set FIREBASE_AUTH_DOMAIN your-firebase-project.firebaseapp.com --scope env
|
|
137
|
+
noodle check --target embedded-assistant src/server.ts
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Assistant origins are exact and HTTPS-only. A local embedding application must serve itself over HTTPS and
|
|
141
|
+
declare an origin such as `https://localhost:3000`; `noodle dev` does not provide TLS for that separate web
|
|
142
|
+
application.
|
|
143
|
+
|
|
144
|
+
Create the backend credential after deployment. The CLI writes it to a mode-0600 file and never prints the
|
|
145
|
+
secret:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
noodle assistant clients create --name web --org noodleseed --app customer-auth --env prod
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Only the Noodle service URL, assistant client ID, and assistant client secret belong in the authenticated
|
|
152
|
+
customer backend. The model URL, model name, and model API key remain managed by the Noodle deployment.
|
|
153
|
+
|
|
154
|
+
The customer's authenticated backend calls `createAssistantSession(...)` from
|
|
155
|
+
`@noodleseed/assistant/server`, passing the already-verified user and browser origin. The browser then uses
|
|
156
|
+
the returned short-lived session through the Web Component or React wrapper:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
pnpm add @noodleseed/assistant
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
```tsx
|
|
163
|
+
import { NoodleAssistant } from '@noodleseed/assistant/react';
|
|
164
|
+
|
|
165
|
+
<NoodleAssistant
|
|
166
|
+
sessionEndpoint="/api/noodle-assistant/session"
|
|
167
|
+
theme="auto"
|
|
168
|
+
onSessionExpired={() => console.info('Assistant session renewed')}
|
|
169
|
+
/>;
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`theme="auto"` follows the SaaS application. The server-level `branding` block is inherited by both MCP App
|
|
173
|
+
widgets and the assistant; documented `--ns-assistant-*` semantic CSS variables remain the final integration
|
|
174
|
+
escape hatch. There is no second assistant branding declaration.
|
|
175
|
+
The end-user UI contains only customer branding.
|
|
176
|
+
Text streams progressively. Expired turns re-exchange through the authenticated backend and retry once;
|
|
177
|
+
consent-bound tool confirmations never replay automatically.
|
|
178
|
+
|
|
179
|
+
The Firebase project ID is required because Firebase ID tokens use the project ID as the token audience and
|
|
180
|
+
issuer suffix. The runtime verifies `aud` against the project ID and `iss` against
|
|
181
|
+
`https://securetoken.google.com/<projectId>`.
|
|
182
|
+
|
|
183
|
+
The Firebase Web API key and auth domain are public Firebase browser configuration. They let the Noodle
|
|
184
|
+
Cloud-hosted bridge initialize Firebase Auth for this customer project; they are not server secrets. Keep
|
|
185
|
+
them out of source with `variable(...)`, restrict the Firebase key to the expected browser origins and APIs,
|
|
186
|
+
and use `secret(...)` only for credentials that must never reach a browser.
|
|
187
|
+
|
|
188
|
+
The `noodleseed_app_api` connector currently points at the NoodleSeed.com dev app surface:
|
|
189
|
+
|
|
190
|
+
```text
|
|
191
|
+
https://dev.noodleseed.com
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
When the customer app moves from `dev.noodleseed.com` to `app.noodleseed.com`, update the connector's
|
|
195
|
+
`noodleseedApiOrigin` constant to the production API origin that serves the same paths.
|
|
196
|
+
|
|
197
|
+
The connector uses delegated Firebase customer credentials. During the customer OAuth bridge, Noodle Seed
|
|
198
|
+
verifies the Firebase ID token, stores the Firebase refresh token through the credential broker, and refreshes
|
|
199
|
+
a Firebase ID token when the connector calls the NoodleSeed.com app API. The broker then exchanges that ID
|
|
200
|
+
token for the app's existing Next.js session cookie. There is no shared `NOODLESEED_APP_API_TOKEN` for this
|
|
201
|
+
example.
|
|
202
|
+
|
|
203
|
+
Firebase Auth must list `cloud.noodleseed.dev` as an authorized domain before browser sign-in works in
|
|
204
|
+
production.
|
|
205
|
+
|
|
206
|
+
## Deploy customer-protected to Noodle Seed Cloud
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
noodle deploy examples/customer-auth/src/server.ts \
|
|
210
|
+
--org noodleseed \
|
|
211
|
+
--app customer-auth \
|
|
212
|
+
--env prod \
|
|
213
|
+
--access customers
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Endpoint:
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
https://cloud.noodleseed.dev/o/noodleseed/customer-auth/mcp
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## MCP Primitives
|
|
223
|
+
|
|
224
|
+
- Tool `list_my_organizations`: calls `GET /api/organizations` and returns the organizations the signed-in
|
|
225
|
+
customer is a member of. Takes no arguments; the org set is scoped by the verified customer session.
|
|
226
|
+
- Tool `list_org_apps`: calls `GET /api/organizations/{org_id}/apps` for one organization `org_id`.
|
|
227
|
+
|
|
228
|
+
## Auth boundary
|
|
229
|
+
|
|
230
|
+
Firebase ID-token verification is handled by Noodle Seed's generic Firebase bridge adapter during OAuth
|
|
231
|
+
issuance. The MCP client receives a Noodle-issued, resource-bound access token marked as a Firebase customer
|
|
232
|
+
identity; raw Firebase tokens and inbound MCP bearer tokens are never forwarded to tools, connectors,
|
|
233
|
+
widgets, or downstream systems.
|
|
234
|
+
|
|
235
|
+
The connector-backed tools use the credential broker to turn the signed-in Firebase customer session into the
|
|
236
|
+
same session-cookie credential that the existing NoodleSeed.com Next.js API already expects. The inbound MCP
|
|
237
|
+
bearer token is never used as an app API credential.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "customer-auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "vitest run",
|
|
8
|
+
"validate": "noodle validate",
|
|
9
|
+
"dev": "noodle dev",
|
|
10
|
+
"deploy": "noodle deploy"
|
|
11
|
+
},
|
|
12
|
+
"devDependencies": {
|
|
13
|
+
"@noodleseed/one": "latest",
|
|
14
|
+
"vitest": "latest"
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
annotations,
|
|
3
|
+
connector,
|
|
4
|
+
customerAuth,
|
|
5
|
+
embeddedAssistant,
|
|
6
|
+
openAICompatible,
|
|
7
|
+
secret,
|
|
8
|
+
server,
|
|
9
|
+
tool,
|
|
10
|
+
variable,
|
|
11
|
+
z,
|
|
12
|
+
} from '@noodleseed/one';
|
|
13
|
+
|
|
14
|
+
const noodleseedApiOrigin = 'https://dev.noodleseed.com';
|
|
15
|
+
|
|
16
|
+
const noodleseedApi = connector('noodleseed_app_api')
|
|
17
|
+
.version('1.0.0')
|
|
18
|
+
.http({
|
|
19
|
+
baseUrl: noodleseedApiOrigin,
|
|
20
|
+
allowedOrigins: [noodleseedApiOrigin],
|
|
21
|
+
auth: {
|
|
22
|
+
kind: 'delegatedSessionCookie',
|
|
23
|
+
provider: 'firebase',
|
|
24
|
+
sessionUrl: `${noodleseedApiOrigin}/api/auth/session`,
|
|
25
|
+
tokenField: 'idToken',
|
|
26
|
+
},
|
|
27
|
+
operations: {
|
|
28
|
+
list_org_apps: {
|
|
29
|
+
type: 'read',
|
|
30
|
+
method: 'GET',
|
|
31
|
+
path: '/api/organizations/${args.org_id}/apps',
|
|
32
|
+
query: ['skip', 'limit'],
|
|
33
|
+
input: z.object({
|
|
34
|
+
org_id: z.string(),
|
|
35
|
+
skip: z.number().optional(),
|
|
36
|
+
limit: z.number().optional(),
|
|
37
|
+
}),
|
|
38
|
+
output: z.object({ result: z.unknown().optional() }),
|
|
39
|
+
response: {
|
|
40
|
+
result: '${response}',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
list_organizations: {
|
|
44
|
+
type: 'read',
|
|
45
|
+
method: 'GET',
|
|
46
|
+
path: '/api/organizations',
|
|
47
|
+
output: z.object({ organizations: z.array(z.unknown()).optional() }),
|
|
48
|
+
response: {
|
|
49
|
+
organizations: '${response.organizations}',
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export default server(
|
|
56
|
+
'noodleseed_customer_auth',
|
|
57
|
+
{
|
|
58
|
+
title: 'NoodleSeed.com Customer Auth',
|
|
59
|
+
version: '1.0.0',
|
|
60
|
+
branding: {
|
|
61
|
+
name: 'Noodle Seed Assistant',
|
|
62
|
+
accent: '#E85D24',
|
|
63
|
+
surface: '#FFFFFF',
|
|
64
|
+
surfaceDark: '#171310',
|
|
65
|
+
colorScheme: 'auto',
|
|
66
|
+
theme: {
|
|
67
|
+
light: { accentText: '#FFFFFF', text: '#1C1714' },
|
|
68
|
+
dark: { accent: '#FF8A4C', accentText: '#1C100A', text: '#FFF8F2' },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
use: { app_api: noodleseedApi },
|
|
72
|
+
auth: customerAuth.firebase({
|
|
73
|
+
projectId: variable('FIREBASE_PROJECT_ID'),
|
|
74
|
+
apiKey: variable('FIREBASE_WEB_API_KEY'),
|
|
75
|
+
authDomain: variable('FIREBASE_AUTH_DOMAIN'),
|
|
76
|
+
user: {
|
|
77
|
+
id: 'sub',
|
|
78
|
+
email: 'email',
|
|
79
|
+
name: 'name',
|
|
80
|
+
tenant: 'firebase.tenant',
|
|
81
|
+
orgs: 'claims.orgs',
|
|
82
|
+
roles: 'claims.roles',
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
instructions:
|
|
86
|
+
'Customer-authenticated demo. Firebase proves the customer identity, while read-only NoodleSeed.com API calls use broker-managed delegated Firebase customer credentials.',
|
|
87
|
+
assistant: embeddedAssistant({
|
|
88
|
+
model: openAICompatible({
|
|
89
|
+
baseUrl: variable('ASSISTANT_MODEL_BASE_URL'),
|
|
90
|
+
model: variable('ASSISTANT_MODEL'),
|
|
91
|
+
apiKey: secret('ASSISTANT_MODEL_API_KEY'),
|
|
92
|
+
}),
|
|
93
|
+
// Production origins are exact HTTPS; http://localhost:<port> is allowed for local development.
|
|
94
|
+
allowedOrigins: [
|
|
95
|
+
'https://app.noodleseed.com',
|
|
96
|
+
'https://dev.noodleseed.com',
|
|
97
|
+
'http://localhost:3000',
|
|
98
|
+
],
|
|
99
|
+
layout: { mode: 'floating', position: 'bottom-right', panelWidth: 420 },
|
|
100
|
+
labels: {
|
|
101
|
+
welcomeHeading: 'How can I help with Noodle Seed?',
|
|
102
|
+
composerPlaceholder: 'Ask about your apps…',
|
|
103
|
+
},
|
|
104
|
+
suggestedPrompts: ['Show my organizations', 'List the apps in my organization'],
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
[
|
|
108
|
+
tool('list_org_apps', {
|
|
109
|
+
description: 'List NoodleSeed.com apps for an organization from the dev app API.',
|
|
110
|
+
input: z.object({
|
|
111
|
+
org_id: z.string(),
|
|
112
|
+
skip: z.number().int().min(0).optional(),
|
|
113
|
+
limit: z.number().int().min(1).max(100).optional(),
|
|
114
|
+
}),
|
|
115
|
+
output: z.object({
|
|
116
|
+
result: z.unknown(),
|
|
117
|
+
}),
|
|
118
|
+
annotations: annotations.readOnly(),
|
|
119
|
+
fulfil({ input, connectors }) {
|
|
120
|
+
const apps = connectors.app_api.listOrgApps({
|
|
121
|
+
org_id: input.org_id,
|
|
122
|
+
skip: input.skip,
|
|
123
|
+
limit: input.limit,
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
result: apps.result,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
}),
|
|
130
|
+
tool('list_my_organizations', {
|
|
131
|
+
description: 'List the NoodleSeed.com organizations the signed-in customer belongs to.',
|
|
132
|
+
input: z.object({}),
|
|
133
|
+
output: z.object({
|
|
134
|
+
organizations: z.array(z.unknown()),
|
|
135
|
+
}),
|
|
136
|
+
annotations: annotations.readOnly(),
|
|
137
|
+
fulfil({ connectors }) {
|
|
138
|
+
const organizations = connectors.app_api.listOrganizations();
|
|
139
|
+
return {
|
|
140
|
+
organizations: organizations.organizations,
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
],
|
|
145
|
+
);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import app from '../src/server.js';
|
|
3
|
+
|
|
4
|
+
describe('customer-auth example', () => {
|
|
5
|
+
it('exports a customer-authenticated, customer-branded embedded assistant', async () => {
|
|
6
|
+
expect(typeof app.toManifest).toBe('function');
|
|
7
|
+
const manifest = await app.toManifest();
|
|
8
|
+
expect(manifest.server.assistant).toMatchObject({
|
|
9
|
+
model: { kind: 'openai-compatible', apiKey: 'ASSISTANT_MODEL_API_KEY' },
|
|
10
|
+
layout: { mode: 'floating' },
|
|
11
|
+
});
|
|
12
|
+
expect(
|
|
13
|
+
manifest.server.assistant?.allowedOrigins.every((origin) => origin.startsWith('https://')),
|
|
14
|
+
).toBe(true);
|
|
15
|
+
expect(manifest.server.branding).toMatchObject({
|
|
16
|
+
name: 'Noodle Seed Assistant',
|
|
17
|
+
colorScheme: 'auto',
|
|
18
|
+
});
|
|
19
|
+
expect(manifest.server.auth).toMatchObject({
|
|
20
|
+
projectId: '${env.FIREBASE_PROJECT_ID}',
|
|
21
|
+
apiKey: '${env.FIREBASE_WEB_API_KEY}',
|
|
22
|
+
authDomain: '${env.FIREBASE_AUTH_DOMAIN}',
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { server, tool, z } from '@noodleseed/one';
|
|
1
|
+
import { annotations, server, tool, z } from '@noodleseed/one';
|
|
2
2
|
|
|
3
3
|
export default server(
|
|
4
4
|
'hello',
|
|
@@ -22,6 +22,8 @@ export default server(
|
|
|
22
22
|
output: z.object({
|
|
23
23
|
message: z.string(),
|
|
24
24
|
}),
|
|
25
|
+
// Read-only, closed-world: assistant surfaces run this without a consent prompt.
|
|
26
|
+
annotations: annotations.readOnly(),
|
|
25
27
|
fulfil: ({ input }) => {
|
|
26
28
|
return { message: `Hello, ${input.name}!` };
|
|
27
29
|
},
|