@noodleseed/agent-kit 0.54.0 → 0.56.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 +251 -251
- package/package.json +1 -1
- package/skills/claude-code/SKILL.md +1 -1
- 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 +184 -155
- package/skills/claude-code/examples/customer-auth/src/server.ts +66 -20
- package/skills/claude-code/examples/customer-auth/test/server.test.ts +51 -9
- 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 +92 -3
- package/skills/claude-code/references/compile-errors.md +9 -0
- package/skills/claude-code/references/examples.md +1 -1
- package/skills/claude-code/references/sdk-surface.md +1 -0
- 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 +1 -1
- 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 +184 -155
- package/skills/codex/examples/customer-auth/src/server.ts +66 -20
- package/skills/codex/examples/customer-auth/test/server.test.ts +51 -9
- 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 +92 -3
- package/skills/codex/references/compile-errors.md +9 -0
- package/skills/codex/references/examples.md +1 -1
- package/skills/codex/references/sdk-surface.md +1 -0
- package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
- package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
|
@@ -1,102 +1,174 @@
|
|
|
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
|
+
reads and confirmed actions 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 and app lifecycle operations:
|
|
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.
|
|
19
|
+
- `archive_org_app` archives one app only after exact runtime confirmation. It requires the
|
|
20
|
+
`org_apps:write` scope and `org_admin` role.
|
|
21
21
|
|
|
22
|
-
The
|
|
23
|
-
`list_org_apps` takes one of those `
|
|
24
|
-
|
|
22
|
+
The tools chain: `list_my_organizations` surfaces the `org_id`s the customer can act on,
|
|
23
|
+
`list_org_apps` takes one of those ids, and `archive_org_app` accepts the selected app id. Tool code remains
|
|
24
|
+
independent of the selected origin.
|
|
25
25
|
|
|
26
|
-
##
|
|
26
|
+
## Declare the customer endpoint
|
|
27
27
|
|
|
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.
|
|
28
|
+
`customerEndpoint` names one private routing authority and bounds the origins an IdP may select:
|
|
33
29
|
|
|
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
|
|
30
|
+
```ts
|
|
31
|
+
const customerApi = customerEndpoint('customer_api', {
|
|
32
|
+
allowedHttpsHostSuffixes: ['api.noodleseed.dev'],
|
|
33
|
+
});
|
|
44
34
|
```
|
|
45
35
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
secrets, proxy or rewrite metadata, rotate credentials, or redeploy repeatedly to conceal the mismatch.
|
|
36
|
+
Use either non-empty `allowedHttpsHostSuffixes` or non-empty `allowedHttpsOrigins`, never both. Exact-origin
|
|
37
|
+
policies may include a non-default port. Suffix policies match only the exact hostname or dot-boundary
|
|
38
|
+
subdomains on port 443. A routed connector must not add `allowedOrigins`; its endpoint policy is the egress
|
|
39
|
+
allowlist.
|
|
51
40
|
|
|
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.
|
|
41
|
+
The connector uses that declaration as its normal base URL. Its token endpoint remains a fixed, independently
|
|
42
|
+
validated HTTPS URL:
|
|
57
43
|
|
|
58
|
-
|
|
44
|
+
```ts
|
|
45
|
+
const api = connector('noodleseed_app_api')
|
|
46
|
+
.version('1.0.0')
|
|
47
|
+
.http({
|
|
48
|
+
baseUrl: customerApi,
|
|
49
|
+
auth: {
|
|
50
|
+
kind: 'delegatedTokenExchange',
|
|
51
|
+
tokenUrl: 'https://id.noodleseed.dev/oauth/token',
|
|
52
|
+
clientId: variable('CUSTOMER_API_CLIENT_ID'),
|
|
53
|
+
clientSecret: secret('CUSTOMER_API_CLIENT_SECRET'),
|
|
54
|
+
scopes: ['organizations:read', 'org_apps:read', 'org_apps:write'],
|
|
55
|
+
audience: 'noodleseed-customer-api',
|
|
56
|
+
},
|
|
57
|
+
operations: {
|
|
58
|
+
// read and action operations...
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
```
|
|
59
62
|
|
|
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.
|
|
63
|
+
## Map the endpoint from verified OIDC
|
|
66
64
|
|
|
67
|
-
The
|
|
65
|
+
The IdP claim contains the complete base URL, including an optional base path. Routing is separate from the
|
|
66
|
+
public `${user}` expression scope:
|
|
68
67
|
|
|
69
68
|
```ts
|
|
70
|
-
auth: customerAuth.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
user: {
|
|
69
|
+
auth: customerAuth.oidc({
|
|
70
|
+
issuer: 'https://id.noodleseed.dev',
|
|
71
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
72
|
+
claims: {
|
|
75
73
|
id: 'sub',
|
|
76
74
|
email: 'email',
|
|
77
75
|
name: 'name',
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
orgs: 'permissions.orgs',
|
|
77
|
+
roles: 'permissions.roles',
|
|
78
|
+
scopes: 'permissions.scopes',
|
|
79
|
+
},
|
|
80
|
+
routing: {
|
|
81
|
+
endpoints: {
|
|
82
|
+
customer_api: { claim: 'tenant.api_base_url' },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For federated OIDC, put the same endpoint map on every issuer. Claim paths may differ, but each issuer must
|
|
89
|
+
map every endpoint the app uses:
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
auth: customerAuth.federatedOidc({
|
|
93
|
+
issuers: [
|
|
94
|
+
{
|
|
95
|
+
issuer: 'https://id.customer-a.com',
|
|
96
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
97
|
+
routing: {
|
|
98
|
+
endpoints: {
|
|
99
|
+
customer_api: { claim: 'tenant.api_base_url' },
|
|
100
|
+
},
|
|
82
101
|
},
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
issuer: 'https://login.customer-b.com',
|
|
105
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
106
|
+
routing: {
|
|
107
|
+
endpoints: {
|
|
108
|
+
customer_api: { claim: 'organization.routes.customer_api' },
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
],
|
|
83
113
|
}),
|
|
84
114
|
```
|
|
85
115
|
|
|
86
|
-
|
|
87
|
-
|
|
116
|
+
At runtime, Noodle Seed validates both the configured audience and exact MCP resource, projects the route
|
|
117
|
+
into private request state, applies its policy, and freezes it for the call. Missing, malformed, or
|
|
118
|
+
disallowed claims return `connector_route_unavailable` before credential lookup or connector egress.
|
|
119
|
+
Resolved URLs never enter artifacts, `${user}`, logs, model output, widgets, public confirmation review,
|
|
120
|
+
broker cache keys, or delegated exchange assertions.
|
|
121
|
+
|
|
122
|
+
Routed reads work in tools, including declared nested calls. Routed actions require exact
|
|
123
|
+
`annotations.confirm: true`; otherwise they fail with `customer_endpoint_action_unsupported`. Routed
|
|
124
|
+
resources, prompts, and ambient context fail with `customer_endpoint_surface_unsupported`.
|
|
125
|
+
|
|
126
|
+
The flagship's routed action uses the normal TypeScript action helper:
|
|
88
127
|
|
|
89
128
|
```ts
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
129
|
+
tool('archive_org_app', {
|
|
130
|
+
authorization: {
|
|
131
|
+
requiredScopes: ['org_apps:write'],
|
|
132
|
+
allowedRoles: ['org_admin'],
|
|
133
|
+
},
|
|
134
|
+
annotations: annotations.openAction({ destructive: false, confirm: true }),
|
|
135
|
+
// input, output, and the normal connectors.app_api.archiveOrgApp(...) call...
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
The flagship also opts into the current stateless hosted MCP path:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
interactions: {
|
|
143
|
+
confirmationFallback: 'host',
|
|
95
144
|
},
|
|
96
145
|
```
|
|
97
146
|
|
|
98
|
-
|
|
99
|
-
|
|
147
|
+
A bidirectional client that negotiated form elicitation can complete the standard confirmation exchange
|
|
148
|
+
instead. The explicit host fallback trusts the MCP host to have collected native write approval before the
|
|
149
|
+
tool call reaches Noodle Seed; it is never inferred from client identity and does not replace auth, policy,
|
|
150
|
+
or accurate action/destructive annotations. Omit the fallback when connected hosts are not trusted to
|
|
151
|
+
provide that approval. If neither standard confirmation nor the fallback is available, the action fails
|
|
152
|
+
closed with `interaction_unavailable`.
|
|
153
|
+
|
|
154
|
+
Preparation stores only sorted route `{ key, fingerprint }` bindings in its private server-held
|
|
155
|
+
continuation; the public review exposes none of them. Acceptance re-resolves the current request route and
|
|
156
|
+
returns `invalid_continuation` if it is missing or changed, before policy, credentials, or egress. A match
|
|
157
|
+
reuses the current frozen snapshot for the action and all nested or later reads.
|
|
158
|
+
|
|
159
|
+
The application developer owns the direct/federated authorization server. It must publish its path-inserted
|
|
160
|
+
RFC 8414 document as direct HTTP 200 JSON with exact issuer and HTTPS authorization/token/registration/JWKS
|
|
161
|
+
endpoints, authorization-code and refresh grants, PKCE S256, public-client auth method `none`, RFC 8707
|
|
162
|
+
resource handling, and public signing keys. Access tokens must include the exact MCP resource audience; there
|
|
163
|
+
is no audience fallback.
|
|
164
|
+
|
|
165
|
+
Run `noodle auth doctor src/server.ts` before sharing. Its bounded, read-only probes never register a client.
|
|
166
|
+
Adding the embedded assistant does not choose or rewrite MCP customer auth.
|
|
167
|
+
|
|
168
|
+
## Per-tool authorization remains independent
|
|
169
|
+
|
|
170
|
+
The mapped `roles` and `scopes` paths are read only after OIDC verification. The restricted tool declares its
|
|
171
|
+
rule beside the rest of its public contract:
|
|
100
172
|
|
|
101
173
|
```ts
|
|
102
174
|
tool('list_org_apps', {
|
|
@@ -109,9 +181,9 @@ tool('list_org_apps', {
|
|
|
109
181
|
```
|
|
110
182
|
|
|
111
183
|
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
|
-
|
|
184
|
+
both conditions apply. Route availability never changes `tools/list`: discovery remains based only on
|
|
185
|
+
roles/scopes. A restricted tool is omitted for an ineligible customer and a guessed direct call still fails
|
|
186
|
+
closed.
|
|
115
187
|
|
|
116
188
|
Tool code calls the connector normally:
|
|
117
189
|
|
|
@@ -127,39 +199,13 @@ fulfil({ input, connectors }) {
|
|
|
127
199
|
}
|
|
128
200
|
```
|
|
129
201
|
|
|
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.
|
|
145
|
-
|
|
146
|
-
```ts
|
|
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
|
-
```
|
|
202
|
+
The broker exchanges a short-lived, platform-signed assertion at the fixed token endpoint and caches the
|
|
203
|
+
result by caller, connector, scopes, and a route fingerprint. The assertion carries only the route key and
|
|
204
|
+
fingerprint, never the URL. The MCP access token is never forwarded to the customer API. The exchange wire
|
|
205
|
+
contract lives in [docs/spec/connectors.md](../../docs/spec/connectors.md).
|
|
155
206
|
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
207
|
+
Firebase and Microsoft remain supported managed adapters; their provider-specific contracts and tests live
|
|
208
|
+
in [docs/spec/auth-and-policy.md](../../docs/spec/auth-and-policy.md) and the SharePoint flagship.
|
|
163
209
|
|
|
164
210
|
## Validate
|
|
165
211
|
|
|
@@ -170,7 +216,7 @@ noodle validate examples/customer-auth/src/server.ts
|
|
|
170
216
|
|
|
171
217
|
Against a deployed customer-protected environment, set a short-lived real customer token only in
|
|
172
218
|
`NOODLE_CUSTOMER_TOKEN` and add `--live --org <org> --app <app> --env <env>`. The live doctor performs
|
|
173
|
-
credential exchanges without invoking
|
|
219
|
+
credential exchanges without invoking any business tool.
|
|
174
220
|
|
|
175
221
|
## Run locally
|
|
176
222
|
|
|
@@ -184,13 +230,16 @@ The embedded assistant uses a customer-supplied OpenAI Chat Completions-compatib
|
|
|
184
230
|
managed values at the Noodle deployment environment; none of these values belongs in the customer web
|
|
185
231
|
application environment, and the API key never reaches the browser:
|
|
186
232
|
|
|
233
|
+
The assistant session carries a verified user, tenant, deployment, roles, and scopes, but not the IdP's
|
|
234
|
+
`tenant.api_base_url` claim. These routed operations are MCP-only; do not copy the route into page context,
|
|
235
|
+
session claims, tool input, or model instructions.
|
|
236
|
+
|
|
187
237
|
```bash
|
|
188
238
|
noodle variables set ASSISTANT_MODEL_BASE_URL https://model.example.com/v1 --scope env
|
|
189
239
|
noodle variables set ASSISTANT_MODEL your-model --scope env
|
|
190
240
|
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
|
|
241
|
+
noodle variables set CUSTOMER_API_CLIENT_ID your-broker-client-id --scope env
|
|
242
|
+
noodle secrets set CUSTOMER_API_CLIENT_SECRET --scope env
|
|
194
243
|
noodle check --target embedded-assistant src/server.ts
|
|
195
244
|
```
|
|
196
245
|
|
|
@@ -198,11 +247,9 @@ Assistant origins are exact. Production embedding origins must use HTTPS; plain
|
|
|
198
247
|
loopback development origins such as `http://localhost:3000`, `http://127.0.0.1:3000`, or
|
|
199
248
|
`http://[::1]:3000`. `noodle dev` serves the MCP project, not that separate embedding application.
|
|
200
249
|
|
|
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.
|
|
250
|
+
The bounded `presentation` object configures the panel, launcher, header, composer, and messages. Its
|
|
251
|
+
primitives derive colors from shared server `branding`; raw HTML, CSS, inline SVG, renderer classes, and
|
|
252
|
+
callbacks are not accepted.
|
|
206
253
|
|
|
207
254
|
Create the backend credential after deployment. The CLI writes it to a mode-0600 file and never prints the
|
|
208
255
|
secret:
|
|
@@ -382,13 +429,15 @@ secret allowlist; regenerate existing framework-owned environment binding types
|
|
|
382
429
|
Devtools/model exercises to synthetic data, and obtain approval before sending real connector data to an
|
|
383
430
|
external model.
|
|
384
431
|
|
|
385
|
-
After deployment,
|
|
386
|
-
a business operation:
|
|
432
|
+
After deployment, use the assistant doctor to verify the embed client, model, and static session boundary:
|
|
387
433
|
|
|
388
434
|
```sh
|
|
389
435
|
noodle assistant doctor --user-id <real-test-user> --origin "$PUBLIC_APP_ORIGIN" --org <org> --app <app> --env <env>
|
|
390
436
|
```
|
|
391
437
|
|
|
438
|
+
This does not add an OIDC customer route. Routed operations succeed only through a direct/federated
|
|
439
|
+
customer-authenticated MCP request with an allowed endpoint claim.
|
|
440
|
+
|
|
392
441
|
If the application deliberately sends a first turn on mount, do not combine a persistent "sent" ref with a
|
|
393
442
|
mount effect. React Strict Mode can abort that provisional request and then suppress the stable remount.
|
|
394
443
|
Schedule the send after the provisional cleanup and settle its promise:
|
|
@@ -437,43 +486,21 @@ assistant.subscribeChat((state) => {
|
|
|
437
486
|
});
|
|
438
487
|
```
|
|
439
488
|
|
|
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.
|
|
489
|
+
`theme="auto"` follows the operating-system preference, not a SaaS-owned toggle. Pass the resolved
|
|
490
|
+
`light`/`dark` theme to `NoodleAssistant` and `NoodleAppView`; updates reach mounted MCP Apps without a
|
|
491
|
+
remount. CSS custom properties inherit through the host, and documented `--ns-assistant-*` variables remain
|
|
492
|
+
the final integration escape hatch. Server `branding` is shared by widgets and the assistant; there is no
|
|
493
|
+
second branding declaration. Text streams progressively. Expired turns re-exchange and retry once;
|
|
494
|
+
confirmations never replay automatically.
|
|
468
495
|
|
|
469
|
-
The
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
example.
|
|
496
|
+
The customer IdP must place the full tenant API base URL in `tenant.api_base_url`. For example, one verified
|
|
497
|
+
customer may receive `https://customer-a.api.noodleseed.dev/v1` and another
|
|
498
|
+
`https://customer-b.api.noodleseed.dev/v1`; both satisfy the declared suffix policy. Application code,
|
|
499
|
+
deployment variables, and connector arguments do not select the tenant route.
|
|
474
500
|
|
|
475
|
-
|
|
476
|
-
|
|
501
|
+
`CUSTOMER_API_CLIENT_ID` and `CUSTOMER_API_CLIENT_SECRET` authenticate only the broker to the fixed exchange
|
|
502
|
+
endpoint. They are not customer API bearer tokens. The exchange endpoint verifies the platform-signed
|
|
503
|
+
subject assertion and mints a short-lived token scoped to the signed-in user and route binding.
|
|
477
504
|
|
|
478
505
|
## Deploy customer-protected to Noodle Seed Cloud
|
|
479
506
|
|
|
@@ -496,14 +523,16 @@ https://cloud.noodleseed.dev/o/noodleseed/customer-auth/mcp
|
|
|
496
523
|
- Tool `list_my_organizations`: calls `GET /api/organizations` and returns the organizations the signed-in
|
|
497
524
|
customer is a member of. Takes no arguments; the org set is scoped by the verified customer session.
|
|
498
525
|
- Tool `list_org_apps`: calls `GET /api/organizations/{org_id}/apps` for one organization `org_id`.
|
|
526
|
+
- Tool `archive_org_app`: after confirmation, calls
|
|
527
|
+
`POST /api/organizations/{org_id}/apps/{app_id}/archive`.
|
|
499
528
|
|
|
500
529
|
## Auth boundary
|
|
501
530
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
widgets, or downstream systems.
|
|
531
|
+
Noodle Seed verifies the configured OIDC issuer, audience, and exact MCP resource before reading identity or
|
|
532
|
+
routing claims. Public caller identity contains the user/role/scope projection; the customer route remains
|
|
533
|
+
private request state.
|
|
506
534
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
535
|
+
Connector-backed tools ask the broker for a route-bound delegated credential; only the endpoint key and
|
|
536
|
+
fingerprint enter broker cache/single-flight state or the assertion. The route claim and inbound MCP bearer
|
|
537
|
+
token never reach tools, connectors, widgets, model output, or downstream systems. Confirmed actions keep
|
|
538
|
+
the same URL-blind binding only in private continuation state and reject acceptance-time drift.
|
|
@@ -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', 'org_apps:write'],
|
|
29
|
+
audience: 'noodleseed-customer-api',
|
|
26
30
|
},
|
|
27
31
|
operations: {
|
|
28
32
|
list_org_apps: {
|
|
@@ -49,6 +53,19 @@ const noodleseedApi = connector('noodleseed_app_api')
|
|
|
49
53
|
organizations: '${response.organizations}',
|
|
50
54
|
},
|
|
51
55
|
},
|
|
56
|
+
archive_org_app: {
|
|
57
|
+
type: 'action',
|
|
58
|
+
method: 'POST',
|
|
59
|
+
path: '/api/organizations/${args.org_id}/apps/${args.app_id}/archive',
|
|
60
|
+
input: z.object({
|
|
61
|
+
org_id: z.string(),
|
|
62
|
+
app_id: z.string(),
|
|
63
|
+
}),
|
|
64
|
+
output: z.object({ archived: z.boolean() }),
|
|
65
|
+
response: {
|
|
66
|
+
archived: '${response.archived}',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
52
69
|
},
|
|
53
70
|
});
|
|
54
71
|
|
|
@@ -69,22 +86,26 @@ export default server(
|
|
|
69
86
|
},
|
|
70
87
|
},
|
|
71
88
|
use: { app_api: noodleseedApi },
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
interactions: { confirmationFallback: 'host' },
|
|
90
|
+
auth: customerAuth.oidc({
|
|
91
|
+
issuer: 'https://id.noodleseed.dev',
|
|
92
|
+
audience: 'https://org.cloud.noodleseed.dev/app/mcp',
|
|
93
|
+
claims: {
|
|
77
94
|
id: 'sub',
|
|
78
95
|
email: 'email',
|
|
79
96
|
name: 'name',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
97
|
+
orgs: 'permissions.orgs',
|
|
98
|
+
roles: 'permissions.roles',
|
|
99
|
+
scopes: 'permissions.scopes',
|
|
100
|
+
},
|
|
101
|
+
routing: {
|
|
102
|
+
endpoints: {
|
|
103
|
+
customer_api: { claim: 'tenant.api_base_url' },
|
|
104
|
+
},
|
|
84
105
|
},
|
|
85
106
|
}),
|
|
86
107
|
instructions:
|
|
87
|
-
'
|
|
108
|
+
'Direct/federated MCP OIDC demo. The customer IdP proves identity and privately selects the tenant API base URL, while the broker supplies delegated credentials and confirmed actions stay bound to the reviewed route.',
|
|
88
109
|
assistant: embeddedAssistant({
|
|
89
110
|
model: openAICompatible({
|
|
90
111
|
baseUrl: variable('ASSISTANT_MODEL_BASE_URL'),
|
|
@@ -111,13 +132,13 @@ export default server(
|
|
|
111
132
|
},
|
|
112
133
|
composer: { leadingIcon: 'brand-mark', shape: 'pill' },
|
|
113
134
|
},
|
|
114
|
-
suggestedPrompts: ['
|
|
135
|
+
suggestedPrompts: ['Explain how to connect this customer-authenticated MCP server'],
|
|
115
136
|
}),
|
|
116
137
|
},
|
|
117
138
|
[
|
|
118
139
|
tool('list_org_apps', {
|
|
119
140
|
title: 'List organization apps',
|
|
120
|
-
description: 'List NoodleSeed.com apps for an organization from
|
|
141
|
+
description: 'List NoodleSeed.com apps for an organization from its customer API.',
|
|
121
142
|
authorization: {
|
|
122
143
|
requiredScopes: ['org_apps:read'],
|
|
123
144
|
allowedRoles: ['org_admin', 'org_member'],
|
|
@@ -147,7 +168,7 @@ export default server(
|
|
|
147
168
|
description: 'List the NoodleSeed.com organizations the signed-in customer belongs to.',
|
|
148
169
|
contextProvider: true,
|
|
149
170
|
input: z.object({}),
|
|
150
|
-
// The
|
|
171
|
+
// The customer API returns every organization for the signed-in customer in one response, with no
|
|
151
172
|
// page parameter to pass through, so the bound is declared on the shape. A customer belongs to a
|
|
152
173
|
// handful of organizations; `noodle check` reports an unbounded list as
|
|
153
174
|
// `tool_design_output_bounds`.
|
|
@@ -162,5 +183,30 @@ export default server(
|
|
|
162
183
|
};
|
|
163
184
|
},
|
|
164
185
|
}),
|
|
186
|
+
tool('archive_org_app', {
|
|
187
|
+
title: 'Archive organization app',
|
|
188
|
+
description: 'Archive one NoodleSeed.com app through its customer API after confirmation.',
|
|
189
|
+
authorization: {
|
|
190
|
+
requiredScopes: ['org_apps:write'],
|
|
191
|
+
allowedRoles: ['org_admin'],
|
|
192
|
+
},
|
|
193
|
+
input: z.object({
|
|
194
|
+
org_id: z.string().meta({ title: 'Organization' }),
|
|
195
|
+
app_id: z.string().meta({ title: 'App' }),
|
|
196
|
+
}),
|
|
197
|
+
output: z.object({
|
|
198
|
+
archived: z.boolean(),
|
|
199
|
+
}),
|
|
200
|
+
annotations: annotations.openAction({ destructive: false, confirm: true }),
|
|
201
|
+
fulfil({ input, connectors }) {
|
|
202
|
+
const result = connectors.app_api.archiveOrgApp({
|
|
203
|
+
org_id: input.org_id,
|
|
204
|
+
app_id: input.app_id,
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
archived: result.archived,
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
165
211
|
],
|
|
166
212
|
);
|