@noodleseed/agent-kit 0.53.0 → 0.55.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 +261 -261
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +2 -2
- package/skills/claude-code/authoring-mcp-servers/SKILL.md +1 -1
- package/skills/claude-code/building-mcp-apps/SKILL.md +1 -1
- package/skills/claude-code/connecting-apis-to-mcp/SKILL.md +1 -1
- package/skills/claude-code/debugging-mcp-delivery/SKILL.md +1 -1
- package/skills/claude-code/deploying-mcp-services/SKILL.md +1 -1
- package/skills/claude-code/designing-mcp-products/SKILL.md +1 -1
- package/skills/claude-code/embedding-mcp-assistants/SKILL.md +1 -1
- package/skills/claude-code/examples/customer-auth/README.md +145 -155
- package/skills/claude-code/examples/customer-auth/src/server.ts +27 -20
- package/skills/claude-code/examples/customer-auth/test/server.test.ts +38 -9
- package/skills/claude-code/examples/food-ordering/README.md +5 -4
- package/skills/claude-code/examples/google-bigquery/README.md +6 -0
- package/skills/claude-code/executing-noodle-plans/SKILL.md +1 -1
- package/skills/claude-code/publishing-mcp-integrations/SKILL.md +1 -1
- package/skills/claude-code/references/authoring-workflow.md +73 -4
- package/skills/claude-code/references/compile-errors.md +8 -0
- package/skills/claude-code/references/connect-an-api.md +1 -1
- package/skills/claude-code/references/deploy-and-ops.md +1 -1
- package/skills/claude-code/references/embedded-assistant.md +3 -6
- package/skills/claude-code/references/sdk-surface.md +1 -0
- package/skills/claude-code/references/troubleshooting.md +1 -1
- package/skills/claude-code/reporting-noodle-feedback/SKILL.md +1 -1
- package/skills/claude-code/verifying-mcp-delivery/SKILL.md +1 -1
- package/skills/codex/SKILL.md +2 -2
- package/skills/codex/authoring-mcp-servers/SKILL.md +1 -1
- package/skills/codex/building-mcp-apps/SKILL.md +1 -1
- package/skills/codex/connecting-apis-to-mcp/SKILL.md +1 -1
- package/skills/codex/debugging-mcp-delivery/SKILL.md +1 -1
- package/skills/codex/deploying-mcp-services/SKILL.md +1 -1
- package/skills/codex/designing-mcp-products/SKILL.md +1 -1
- package/skills/codex/embedding-mcp-assistants/SKILL.md +1 -1
- package/skills/codex/examples/customer-auth/README.md +145 -155
- package/skills/codex/examples/customer-auth/src/server.ts +27 -20
- package/skills/codex/examples/customer-auth/test/server.test.ts +38 -9
- package/skills/codex/examples/food-ordering/README.md +5 -4
- package/skills/codex/examples/google-bigquery/README.md +6 -0
- package/skills/codex/executing-noodle-plans/SKILL.md +1 -1
- package/skills/codex/publishing-mcp-integrations/SKILL.md +1 -1
- package/skills/codex/references/authoring-workflow.md +73 -4
- package/skills/codex/references/compile-errors.md +8 -0
- package/skills/codex/references/connect-an-api.md +1 -1
- package/skills/codex/references/deploy-and-ops.md +1 -1
- package/skills/codex/references/embedded-assistant.md +3 -6
- package/skills/codex/references/sdk-surface.md +1 -0
- package/skills/codex/references/troubleshooting.md +1 -1
- package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
- package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
|
@@ -1,102 +1,138 @@
|
|
|
1
|
-
# Customer Auth -
|
|
1
|
+
# Customer Auth - OIDC identity and customer-routed APIs
|
|
2
2
|
|
|
3
|
-
This curated example owns the customer/end-user authentication
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This curated example owns the customer/end-user authentication capability slot. It proves that a SaaS app
|
|
4
|
+
can protect an MCP endpoint with direct OIDC, retain role/scope-based tool authorization, and route ordinary
|
|
5
|
+
read-only connector calls to the API origin selected by the verified customer's identity provider.
|
|
6
6
|
|
|
7
|
-
It also owns the embedded-assistant showcase
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
second skill bundle. The standard embedded element also hosts linked MCP Apps behind its sandbox bridge.
|
|
7
|
+
It also owns the customer-branded embedded-assistant presentation showcase. Embedded sessions lack
|
|
8
|
+
direct/federated MCP OIDC endpoint claims, so these routed tools fail with `connector_route_unavailable`;
|
|
9
|
+
static connectors still support embedded delegated exchange. Exercise routed tools through the MCP endpoint.
|
|
11
10
|
|
|
12
|
-
The public developer entrypoint is [`src/server.ts`](src/server.ts). It
|
|
13
|
-
|
|
14
|
-
surface for org discovery:
|
|
11
|
+
The public developer entrypoint is [`src/server.ts`](src/server.ts). It exposes a deliberately small MCP
|
|
12
|
+
surface for organization discovery:
|
|
15
13
|
|
|
16
14
|
- `list_my_organizations` lists the NoodleSeed.com organizations the signed-in customer belongs to (no
|
|
17
15
|
arguments — the org set comes from the verified customer session).
|
|
18
|
-
- `list_org_apps` lists apps for one of those organizations through
|
|
16
|
+
- `list_org_apps` lists apps for one of those organizations through that tenant's API. It is visible and
|
|
19
17
|
callable only when the verified customer has the `org_apps:read` scope and either the `org_admin` or
|
|
20
18
|
`org_member` role.
|
|
21
19
|
|
|
22
20
|
The two tools chain: `list_my_organizations` surfaces the `org_id`s the customer can act on, and
|
|
23
|
-
`list_org_apps` takes one of those `org_id`s.
|
|
24
|
-
is an ordinary authored HTTP connector.
|
|
21
|
+
`list_org_apps` takes one of those `org_id`s. Tool code remains independent of the selected origin.
|
|
25
22
|
|
|
26
|
-
##
|
|
23
|
+
## Declare the customer endpoint
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
`customerAuth.federatedOidc(...)`, the app developer owns the authorization server. It must publish the
|
|
30
|
-
primary path-inserted RFC 8414 URL as direct HTTP 200 JSON with the exact issuer, HTTPS authorization/token/
|
|
31
|
-
registration/JWKS endpoints, authorization-code and refresh grants, PKCE S256, public-client auth method
|
|
32
|
-
`none`, RFC 8707 resource handling, an access-token `aud` equal to the exact MCP URL, and public signing keys.
|
|
25
|
+
`customerEndpoint` names one private routing authority and bounds the origins an IdP may select:
|
|
33
26
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
Adding the embedded assistant does not choose or rewrite MCP customer auth. Inspect the exact active
|
|
40
|
-
deployment before changing configuration:
|
|
41
|
-
|
|
42
|
-
```sh
|
|
43
|
-
noodle deployments list --org <org> --app <app> --env <env> --json
|
|
27
|
+
```ts
|
|
28
|
+
const customerApi = customerEndpoint('customer_api', {
|
|
29
|
+
allowedHttpsHostSuffixes: ['api.noodleseed.dev'],
|
|
30
|
+
});
|
|
44
31
|
```
|
|
45
32
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
secrets, proxy or rewrite metadata, rotate credentials, or redeploy repeatedly to conceal the mismatch.
|
|
33
|
+
Use either non-empty `allowedHttpsHostSuffixes` or non-empty `allowedHttpsOrigins`, never both. Exact-origin
|
|
34
|
+
policies may include a non-default port. Suffix policies match only the exact hostname or dot-boundary
|
|
35
|
+
subdomains on port 443. A routed connector must not add `allowedOrigins`; its endpoint policy is the egress
|
|
36
|
+
allowlist.
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
authorization route. The SaaS operator only configures Firebase Auth to allow the Noodle Cloud origin, and
|
|
55
|
-
Noodle Cloud signs the customer in with Firebase before posting the Firebase ID token back to its own bridge
|
|
56
|
-
callback.
|
|
38
|
+
The connector uses that declaration as its normal base URL. Its token endpoint remains a fixed, independently
|
|
39
|
+
validated HTTPS URL:
|
|
57
40
|
|
|
58
|
-
|
|
41
|
+
```ts
|
|
42
|
+
const api = connector('noodleseed_app_api')
|
|
43
|
+
.version('1.0.0')
|
|
44
|
+
.http({
|
|
45
|
+
baseUrl: customerApi,
|
|
46
|
+
auth: {
|
|
47
|
+
kind: 'delegatedTokenExchange',
|
|
48
|
+
tokenUrl: 'https://id.noodleseed.dev/oauth/token',
|
|
49
|
+
clientId: variable('CUSTOMER_API_CLIENT_ID'),
|
|
50
|
+
clientSecret: secret('CUSTOMER_API_CLIENT_SECRET'),
|
|
51
|
+
scopes: ['organizations:read', 'org_apps:read'],
|
|
52
|
+
audience: 'noodleseed-customer-api',
|
|
53
|
+
},
|
|
54
|
+
operations: {
|
|
55
|
+
// read operations...
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
Core-v2 account-selecting connector aliases: those declare catalog `credentialProfiles` and bind each alias
|
|
62
|
-
with `bind(connector, { profile, connection })`. Hosted bound `managedSecret(...)` connections are runnable;
|
|
63
|
-
bound `externalExchange()` and `clientCredentials(...)` currently compile as portable metadata but fail
|
|
64
|
-
closed before secret or network access until their provider slice lands. Do not replace this example's
|
|
65
|
-
working delegated-session-cookie path with either unsupported bound exchange source.
|
|
60
|
+
## Map the endpoint from verified OIDC
|
|
66
61
|
|
|
67
|
-
The
|
|
62
|
+
The IdP claim contains the complete base URL, including an optional base path. Routing is separate from the
|
|
63
|
+
public `${user}` expression scope:
|
|
68
64
|
|
|
69
65
|
```ts
|
|
70
|
-
auth: customerAuth.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
user: {
|
|
66
|
+
auth: customerAuth.oidc({
|
|
67
|
+
issuer: 'https://id.noodleseed.dev',
|
|
68
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
69
|
+
claims: {
|
|
75
70
|
id: 'sub',
|
|
76
71
|
email: 'email',
|
|
77
72
|
name: 'name',
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
orgs: 'permissions.orgs',
|
|
74
|
+
roles: 'permissions.roles',
|
|
75
|
+
scopes: 'permissions.scopes',
|
|
76
|
+
},
|
|
77
|
+
routing: {
|
|
78
|
+
endpoints: {
|
|
79
|
+
customer_api: { claim: 'tenant.api_base_url' },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
83
82
|
}),
|
|
84
83
|
```
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
For federated OIDC, put the same endpoint map on every issuer. Claim paths may differ, but each issuer must
|
|
86
|
+
map every endpoint the app uses:
|
|
88
87
|
|
|
89
88
|
```ts
|
|
90
|
-
auth: {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
auth: customerAuth.federatedOidc({
|
|
90
|
+
issuers: [
|
|
91
|
+
{
|
|
92
|
+
issuer: 'https://id.customer-a.com',
|
|
93
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
94
|
+
routing: {
|
|
95
|
+
endpoints: {
|
|
96
|
+
customer_api: { claim: 'tenant.api_base_url' },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
issuer: 'https://login.customer-b.com',
|
|
102
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
103
|
+
routing: {
|
|
104
|
+
endpoints: {
|
|
105
|
+
customer_api: { claim: 'organization.routes.customer_api' },
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
}),
|
|
96
111
|
```
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
At runtime, Noodle Seed validates both the configured audience and exact MCP resource, projects the route
|
|
114
|
+
into private request state, applies its policy, and freezes it for the call. Missing, malformed, or
|
|
115
|
+
disallowed claims return `connector_route_unavailable` before credential lookup or connector egress.
|
|
116
|
+
Resolved URLs never enter artifacts, `${user}`, logs, model output, widgets, confirmation review, broker cache
|
|
117
|
+
keys, or delegated exchange assertions.
|
|
118
|
+
|
|
119
|
+
This release permits routed reads in tools, including declared nested calls. Routed actions fail with
|
|
120
|
+
`customer_endpoint_action_unsupported`; routed resources, prompts, and ambient context fail with
|
|
121
|
+
`customer_endpoint_surface_unsupported`.
|
|
122
|
+
|
|
123
|
+
The application developer owns the direct/federated authorization server. It must publish its path-inserted
|
|
124
|
+
RFC 8414 document as direct HTTP 200 JSON with exact issuer and HTTPS authorization/token/registration/JWKS
|
|
125
|
+
endpoints, authorization-code and refresh grants, PKCE S256, public-client auth method `none`, RFC 8707
|
|
126
|
+
resource handling, and public signing keys. Access tokens must include the exact MCP resource audience; there
|
|
127
|
+
is no audience fallback.
|
|
128
|
+
|
|
129
|
+
Run `noodle auth doctor src/server.ts` before sharing. Its bounded, read-only probes never register a client.
|
|
130
|
+
Adding the embedded assistant does not choose or rewrite MCP customer auth.
|
|
131
|
+
|
|
132
|
+
## Per-tool authorization remains independent
|
|
133
|
+
|
|
134
|
+
The mapped `roles` and `scopes` paths are read only after OIDC verification. The restricted tool declares its
|
|
135
|
+
rule beside the rest of its public contract:
|
|
100
136
|
|
|
101
137
|
```ts
|
|
102
138
|
tool('list_org_apps', {
|
|
@@ -109,9 +145,9 @@ tool('list_org_apps', {
|
|
|
109
145
|
```
|
|
110
146
|
|
|
111
147
|
Every required scope must be present and at least one allowed role must match. When both lists are declared,
|
|
112
|
-
both conditions apply.
|
|
113
|
-
|
|
114
|
-
|
|
148
|
+
both conditions apply. Route availability never changes `tools/list`: discovery remains based only on
|
|
149
|
+
roles/scopes. A restricted tool is omitted for an ineligible customer and a guessed direct call still fails
|
|
150
|
+
closed.
|
|
115
151
|
|
|
116
152
|
Tool code calls the connector normally:
|
|
117
153
|
|
|
@@ -127,39 +163,13 @@ fulfil({ input, connectors }) {
|
|
|
127
163
|
}
|
|
128
164
|
```
|
|
129
165
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
access token remains a Noodle-issued resource-bound token and is never sent to the downstream API.
|
|
135
|
-
|
|
136
|
-
## Delegated downstream auth for your own API (token exchange)
|
|
137
|
-
|
|
138
|
-
The Firebase path above only works for Firebase-session downstreams. When the downstream is **your own
|
|
139
|
-
API** with its own token issuance, use `delegatedTokenExchange` instead
|
|
140
|
-
([ADR 0152](../../docs/decisions/0152-delegated-token-exchange-connector-auth.md)): the platform signs a
|
|
141
|
-
short-lived, JWKS-verifiable assertion of the signed-in user and exchanges it (RFC 8693) at a token
|
|
142
|
-
endpoint you implement, which mints your own user-scoped token — so your API enforces its own per-user
|
|
143
|
-
authorization on every call. It works for verified direct/federated OIDC, built-in provider identities, and
|
|
144
|
-
embedded assistant sessions, with no per-user OAuth enrollment.
|
|
166
|
+
The broker exchanges a short-lived, platform-signed assertion at the fixed token endpoint and caches the
|
|
167
|
+
result by caller, connector, scopes, and a route fingerprint. The assertion carries only the route key and
|
|
168
|
+
fingerprint, never the URL. The MCP access token is never forwarded to the customer API. The exchange wire
|
|
169
|
+
contract lives in [docs/spec/connectors.md](../../docs/spec/connectors.md).
|
|
145
170
|
|
|
146
|
-
|
|
147
|
-
auth
|
|
148
|
-
kind: 'delegatedTokenExchange',
|
|
149
|
-
tokenUrl: 'https://app.example.com/api/assistant/oauth/token', // origin must be in allowedOrigins
|
|
150
|
-
clientId: variable('EXAMPLE_DELEG_CLIENT_ID'),
|
|
151
|
-
clientSecret: secret('EXAMPLE_DELEG_CLIENT_SECRET'),
|
|
152
|
-
scopes: ['time_off'],
|
|
153
|
-
},
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Your endpoint authenticates the broker's client credential, verifies the `subject_token` JWT against the
|
|
157
|
-
platform issuer JWKS (claims include the verified `sub`, `email`, `name`, declared session `claims`,
|
|
158
|
-
`tenant`, and `deployment`), mints a short-lived user-scoped token, and returns the standard
|
|
159
|
-
`{ access_token, token_type, expires_in }` response. The exact wire contract and a copyable endpoint
|
|
160
|
-
implementation live in [docs/spec/connectors.md](../../docs/spec/connectors.md) and the Agent Kit
|
|
161
|
-
authoring-workflow reference ("Delegated downstream auth"). `noodle auth doctor` reports each declared
|
|
162
|
-
exchange endpoint.
|
|
171
|
+
Firebase and Microsoft remain supported managed adapters; their provider-specific contracts and tests live
|
|
172
|
+
in [docs/spec/auth-and-policy.md](../../docs/spec/auth-and-policy.md) and the SharePoint flagship.
|
|
163
173
|
|
|
164
174
|
## Validate
|
|
165
175
|
|
|
@@ -184,13 +194,16 @@ The embedded assistant uses a customer-supplied OpenAI Chat Completions-compatib
|
|
|
184
194
|
managed values at the Noodle deployment environment; none of these values belongs in the customer web
|
|
185
195
|
application environment, and the API key never reaches the browser:
|
|
186
196
|
|
|
197
|
+
The assistant session carries a verified user, tenant, deployment, roles, and scopes, but not the IdP's
|
|
198
|
+
`tenant.api_base_url` claim. These routed operations are MCP-only; do not copy the route into page context,
|
|
199
|
+
session claims, tool input, or model instructions.
|
|
200
|
+
|
|
187
201
|
```bash
|
|
188
202
|
noodle variables set ASSISTANT_MODEL_BASE_URL https://model.example.com/v1 --scope env
|
|
189
203
|
noodle variables set ASSISTANT_MODEL your-model --scope env
|
|
190
204
|
noodle secrets set ASSISTANT_MODEL_API_KEY --scope env
|
|
191
|
-
noodle variables set
|
|
192
|
-
noodle
|
|
193
|
-
noodle variables set FIREBASE_AUTH_DOMAIN your-firebase-project.firebaseapp.com --scope env
|
|
205
|
+
noodle variables set CUSTOMER_API_CLIENT_ID your-broker-client-id --scope env
|
|
206
|
+
noodle secrets set CUSTOMER_API_CLIENT_SECRET --scope env
|
|
194
207
|
noodle check --target embedded-assistant src/server.ts
|
|
195
208
|
```
|
|
196
209
|
|
|
@@ -198,11 +211,9 @@ Assistant origins are exact. Production embedding origins must use HTTPS; plain
|
|
|
198
211
|
loopback development origins such as `http://localhost:3000`, `http://127.0.0.1:3000`, or
|
|
199
212
|
`http://[::1]:3000`. `noodle dev` serves the MCP project, not that separate embedding application.
|
|
200
213
|
|
|
201
|
-
The
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
require no edits to `@noodleseed/assistant`; raw HTML, CSS, inline SVG, renderer class names, and callbacks
|
|
205
|
-
are intentionally not accepted in deployment configuration.
|
|
214
|
+
The bounded `presentation` object configures the panel, launcher, header, composer, and messages. Its
|
|
215
|
+
primitives derive colors from shared server `branding`; raw HTML, CSS, inline SVG, renderer classes, and
|
|
216
|
+
callbacks are not accepted.
|
|
206
217
|
|
|
207
218
|
Create the backend credential after deployment. The CLI writes it to a mode-0600 file and never prints the
|
|
208
219
|
secret:
|
|
@@ -382,13 +393,15 @@ secret allowlist; regenerate existing framework-owned environment binding types
|
|
|
382
393
|
Devtools/model exercises to synthetic data, and obtain approval before sending real connector data to an
|
|
383
394
|
external model.
|
|
384
395
|
|
|
385
|
-
After deployment,
|
|
386
|
-
a business operation:
|
|
396
|
+
After deployment, use the assistant doctor to verify the embed client, model, and static session boundary:
|
|
387
397
|
|
|
388
398
|
```sh
|
|
389
399
|
noodle assistant doctor --user-id <real-test-user> --origin "$PUBLIC_APP_ORIGIN" --org <org> --app <app> --env <env>
|
|
390
400
|
```
|
|
391
401
|
|
|
402
|
+
This does not add an OIDC customer route. Routed operations succeed only through a direct/federated
|
|
403
|
+
customer-authenticated MCP request with an allowed endpoint claim.
|
|
404
|
+
|
|
392
405
|
If the application deliberately sends a first turn on mount, do not combine a persistent "sent" ref with a
|
|
393
406
|
mount effect. React Strict Mode can abort that provisional request and then suppress the stable remount.
|
|
394
407
|
Schedule the send after the provisional cleanup and settle its promise:
|
|
@@ -437,43 +450,21 @@ assistant.subscribeChat((state) => {
|
|
|
437
450
|
});
|
|
438
451
|
```
|
|
439
452
|
|
|
440
|
-
`theme="auto"` follows the
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
declaration.
|
|
447
|
-
The end-user UI contains only customer branding.
|
|
448
|
-
Text streams progressively. Expired turns re-exchange through the authenticated backend and retry once;
|
|
449
|
-
consent-bound tool confirmations never replay automatically.
|
|
450
|
-
|
|
451
|
-
The Firebase project ID is required because Firebase ID tokens use the project ID as the token audience and
|
|
452
|
-
issuer suffix. The runtime verifies `aud` against the project ID and `iss` against
|
|
453
|
-
`https://securetoken.google.com/<projectId>`.
|
|
454
|
-
|
|
455
|
-
The Firebase Web API key and auth domain are public Firebase browser configuration. They let the Noodle
|
|
456
|
-
Cloud-hosted bridge initialize Firebase Auth for this customer project; they are not server secrets. Keep
|
|
457
|
-
them out of source with `variable(...)`, restrict the Firebase key to the expected browser origins and APIs,
|
|
458
|
-
and use `secret(...)` only for credentials that must never reach a browser.
|
|
459
|
-
|
|
460
|
-
The `noodleseed_app_api` connector currently points at the NoodleSeed.com dev app surface:
|
|
461
|
-
|
|
462
|
-
```text
|
|
463
|
-
https://dev.noodleseed.com
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
When the customer app moves from `dev.noodleseed.com` to `app.noodleseed.com`, update the connector's
|
|
467
|
-
`noodleseedApiOrigin` constant to the production API origin that serves the same paths.
|
|
453
|
+
`theme="auto"` follows the operating-system preference, not a SaaS-owned toggle. Pass the resolved
|
|
454
|
+
`light`/`dark` theme to `NoodleAssistant` and `NoodleAppView`; updates reach mounted MCP Apps without a
|
|
455
|
+
remount. CSS custom properties inherit through the host, and documented `--ns-assistant-*` variables remain
|
|
456
|
+
the final integration escape hatch. Server `branding` is shared by widgets and the assistant; there is no
|
|
457
|
+
second branding declaration. Text streams progressively. Expired turns re-exchange and retry once;
|
|
458
|
+
confirmations never replay automatically.
|
|
468
459
|
|
|
469
|
-
The
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
example.
|
|
460
|
+
The customer IdP must place the full tenant API base URL in `tenant.api_base_url`. For example, one verified
|
|
461
|
+
customer may receive `https://customer-a.api.noodleseed.dev/v1` and another
|
|
462
|
+
`https://customer-b.api.noodleseed.dev/v1`; both satisfy the declared suffix policy. Application code,
|
|
463
|
+
deployment variables, and connector arguments do not select the tenant route.
|
|
474
464
|
|
|
475
|
-
|
|
476
|
-
|
|
465
|
+
`CUSTOMER_API_CLIENT_ID` and `CUSTOMER_API_CLIENT_SECRET` authenticate only the broker to the fixed exchange
|
|
466
|
+
endpoint. They are not customer API bearer tokens. The exchange endpoint verifies the platform-signed
|
|
467
|
+
subject assertion and mints a short-lived token scoped to the signed-in user and route binding.
|
|
477
468
|
|
|
478
469
|
## Deploy customer-protected to Noodle Seed Cloud
|
|
479
470
|
|
|
@@ -499,11 +490,10 @@ https://cloud.noodleseed.dev/o/noodleseed/customer-auth/mcp
|
|
|
499
490
|
|
|
500
491
|
## Auth boundary
|
|
501
492
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
widgets, or downstream systems.
|
|
493
|
+
Noodle Seed verifies the configured OIDC issuer, audience, and exact MCP resource before reading identity or
|
|
494
|
+
routing claims. Public caller identity contains the user/role/scope projection; the customer route remains
|
|
495
|
+
private request state.
|
|
506
496
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
497
|
+
Connector-backed tools ask the broker for a route-bound delegated credential; only the endpoint key and
|
|
498
|
+
fingerprint enter broker cache/single-flight state or the assertion. The route claim and inbound MCP bearer
|
|
499
|
+
token never reach tools, connectors, widgets, model output, or downstream systems.
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
annotations,
|
|
3
3
|
connector,
|
|
4
4
|
customerAuth,
|
|
5
|
+
customerEndpoint,
|
|
5
6
|
embeddedAssistant,
|
|
6
7
|
openAICompatible,
|
|
7
8
|
secret,
|
|
@@ -11,18 +12,21 @@ import {
|
|
|
11
12
|
z,
|
|
12
13
|
} from '@noodleseed/one';
|
|
13
14
|
|
|
14
|
-
const
|
|
15
|
+
const customerApi = customerEndpoint('customer_api', {
|
|
16
|
+
allowedHttpsHostSuffixes: ['api.noodleseed.dev'],
|
|
17
|
+
});
|
|
15
18
|
|
|
16
19
|
const noodleseedApi = connector('noodleseed_app_api')
|
|
17
20
|
.version('1.0.0')
|
|
18
21
|
.http({
|
|
19
|
-
baseUrl:
|
|
20
|
-
allowedOrigins: [noodleseedApiOrigin],
|
|
22
|
+
baseUrl: customerApi,
|
|
21
23
|
auth: {
|
|
22
|
-
kind: '
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
kind: 'delegatedTokenExchange',
|
|
25
|
+
tokenUrl: 'https://id.noodleseed.dev/oauth/token',
|
|
26
|
+
clientId: variable('CUSTOMER_API_CLIENT_ID'),
|
|
27
|
+
clientSecret: secret('CUSTOMER_API_CLIENT_SECRET'),
|
|
28
|
+
scopes: ['organizations:read', 'org_apps:read'],
|
|
29
|
+
audience: 'noodleseed-customer-api',
|
|
26
30
|
},
|
|
27
31
|
operations: {
|
|
28
32
|
list_org_apps: {
|
|
@@ -69,22 +73,25 @@ export default server(
|
|
|
69
73
|
},
|
|
70
74
|
},
|
|
71
75
|
use: { app_api: noodleseedApi },
|
|
72
|
-
auth: customerAuth.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
user: {
|
|
76
|
+
auth: customerAuth.oidc({
|
|
77
|
+
issuer: 'https://id.noodleseed.dev',
|
|
78
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
79
|
+
claims: {
|
|
77
80
|
id: 'sub',
|
|
78
81
|
email: 'email',
|
|
79
82
|
name: 'name',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
orgs: 'permissions.orgs',
|
|
84
|
+
roles: 'permissions.roles',
|
|
85
|
+
scopes: 'permissions.scopes',
|
|
86
|
+
},
|
|
87
|
+
routing: {
|
|
88
|
+
endpoints: {
|
|
89
|
+
customer_api: { claim: 'tenant.api_base_url' },
|
|
90
|
+
},
|
|
84
91
|
},
|
|
85
92
|
}),
|
|
86
93
|
instructions:
|
|
87
|
-
'
|
|
94
|
+
'Direct/federated MCP OIDC demo. The customer IdP proves identity and privately selects the tenant API base URL for read-only MCP calls, while the broker supplies delegated credentials.',
|
|
88
95
|
assistant: embeddedAssistant({
|
|
89
96
|
model: openAICompatible({
|
|
90
97
|
baseUrl: variable('ASSISTANT_MODEL_BASE_URL'),
|
|
@@ -111,13 +118,13 @@ export default server(
|
|
|
111
118
|
},
|
|
112
119
|
composer: { leadingIcon: 'brand-mark', shape: 'pill' },
|
|
113
120
|
},
|
|
114
|
-
suggestedPrompts: ['
|
|
121
|
+
suggestedPrompts: ['Explain how to connect this customer-authenticated MCP server'],
|
|
115
122
|
}),
|
|
116
123
|
},
|
|
117
124
|
[
|
|
118
125
|
tool('list_org_apps', {
|
|
119
126
|
title: 'List organization apps',
|
|
120
|
-
description: 'List NoodleSeed.com apps for an organization from
|
|
127
|
+
description: 'List NoodleSeed.com apps for an organization from its customer API.',
|
|
121
128
|
authorization: {
|
|
122
129
|
requiredScopes: ['org_apps:read'],
|
|
123
130
|
allowedRoles: ['org_admin', 'org_member'],
|
|
@@ -147,7 +154,7 @@ export default server(
|
|
|
147
154
|
description: 'List the NoodleSeed.com organizations the signed-in customer belongs to.',
|
|
148
155
|
contextProvider: true,
|
|
149
156
|
input: z.object({}),
|
|
150
|
-
// The
|
|
157
|
+
// The customer API returns every organization for the signed-in customer in one response, with no
|
|
151
158
|
// page parameter to pass through, so the bound is declared on the shape. A customer belongs to a
|
|
152
159
|
// handful of organizations; `noodle check` reports an unbounded list as
|
|
153
160
|
// `tool_design_output_bounds`.
|
|
@@ -23,17 +23,46 @@ describe('customer-auth example', () => {
|
|
|
23
23
|
name: 'Noodle Seed Assistant',
|
|
24
24
|
colorScheme: 'auto',
|
|
25
25
|
});
|
|
26
|
-
expect(manifest.server.auth).
|
|
27
|
-
kind: '
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
expect(manifest.server.auth).toEqual({
|
|
27
|
+
kind: 'oidc',
|
|
28
|
+
issuer: 'https://id.noodleseed.dev',
|
|
29
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
30
|
+
claims: {
|
|
31
|
+
id: 'sub',
|
|
32
|
+
email: 'email',
|
|
33
|
+
name: 'name',
|
|
34
|
+
orgs: 'permissions.orgs',
|
|
35
|
+
roles: 'permissions.roles',
|
|
36
|
+
scopes: 'permissions.scopes',
|
|
37
|
+
},
|
|
38
|
+
routing: {
|
|
39
|
+
endpoints: {
|
|
40
|
+
customer_api: { claim: 'tenant.api_base_url' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
const catalog = app.toConnectorCatalog();
|
|
45
|
+
expect(catalog?.connectors).toHaveLength(1);
|
|
46
|
+
expect(catalog?.connectors[0]?.http).toMatchObject({
|
|
47
|
+
baseUrl: {
|
|
48
|
+
kind: 'customerEndpoint',
|
|
49
|
+
name: 'customer_api',
|
|
50
|
+
policy: { allowedHttpsHostSuffixes: ['api.noodleseed.dev'] },
|
|
51
|
+
},
|
|
52
|
+
auth: {
|
|
53
|
+
kind: 'delegatedTokenExchange',
|
|
54
|
+
tokenUrl: 'https://id.noodleseed.dev/oauth/token',
|
|
55
|
+
clientId: '${env.CUSTOMER_API_CLIENT_ID}',
|
|
56
|
+
clientSecret: 'CUSTOMER_API_CLIENT_SECRET',
|
|
35
57
|
},
|
|
36
58
|
});
|
|
59
|
+
expect(
|
|
60
|
+
Object.values(catalog?.connectors[0]?.http?.operations ?? {}).every(
|
|
61
|
+
(operation) => operation.type === 'read',
|
|
62
|
+
),
|
|
63
|
+
).toBe(true);
|
|
64
|
+
expect(catalog?.connectors[0]?.http).not.toHaveProperty('allowedOrigins');
|
|
65
|
+
expect(JSON.stringify({ manifest, catalog })).not.toContain('tenant-a.api.noodleseed.dev');
|
|
37
66
|
expect(manifest.tools.find((tool) => tool.name === 'list_org_apps')?.authorization).toEqual({
|
|
38
67
|
requiredScopes: ['org_apps:read'],
|
|
39
68
|
allowedRoles: ['org_admin', 'org_member'],
|
|
@@ -84,13 +84,14 @@ noodle connect inspector
|
|
|
84
84
|
## Deploy
|
|
85
85
|
|
|
86
86
|
```sh
|
|
87
|
-
noodle
|
|
88
|
-
noodle deploy --access owner-only
|
|
87
|
+
noodle deploy --org demo --app food-ordering --env prod --access owner-only
|
|
89
88
|
noodle open
|
|
90
89
|
```
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
That one deploy command preflights the complete target, creates a missing app/environment, and verifies
|
|
92
|
+
hosted readiness. If it is interrupted, rerun the same command to resume the unfinished operation without a
|
|
93
|
+
duplicate deployment. Use `--access org-members` for an org-wide internal demo. This example has no
|
|
94
|
+
connector secrets and does not include tokens, caller-key mechanisms, or `.env.noodle` values.
|
|
94
95
|
|
|
95
96
|
## Demo Assets
|
|
96
97
|
|
|
@@ -99,3 +99,9 @@ Compilation and tests need no Google credentials:
|
|
|
99
99
|
noodle validate
|
|
100
100
|
noodle test
|
|
101
101
|
```
|
|
102
|
+
|
|
103
|
+
For a local run that needs the declared provider values, the exact project-root `.env` can contain
|
|
104
|
+
`GOOGLE_WIF_PROVIDER` and `GOOGLE_SERVICE_ACCOUNT`; `noodle dev` uses matching declarations only as a
|
|
105
|
+
read-only fallback, and scoped `.env.noodle` values override it. Never commit or ask an agent to read either
|
|
106
|
+
file. Interactive deploy can offer a default-No import of matching missing names to the visible target;
|
|
107
|
+
non-interactive and plugin deploys keep the value-free `noodle variables set ... --from-env` recovery path.
|
|
@@ -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.
|
|
6
|
+
<!-- noodle-skill version:0.55.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.
|
|
6
|
+
<!-- noodle-skill version:0.55.0 hash:efffbf82007f935d -->
|
|
7
7
|
|
|
8
8
|
# publishing-mcp-integrations
|
|
9
9
|
|