@noodleseed/agent-kit 0.55.0 → 0.56.1
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 +249 -249
- 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 +65 -23
- package/skills/claude-code/examples/customer-auth/src/server.ts +42 -3
- package/skills/claude-code/examples/customer-auth/test/server.test.ts +19 -6
- 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 +26 -6
- package/skills/claude-code/references/compile-errors.md +2 -1
- package/skills/claude-code/references/examples.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 +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 +65 -23
- package/skills/codex/examples/customer-auth/src/server.ts +42 -3
- package/skills/codex/examples/customer-auth/test/server.test.ts +19 -6
- 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 +26 -6
- package/skills/codex/references/compile-errors.md +2 -1
- package/skills/codex/references/examples.md +1 -1
- package/skills/codex/reporting-noodle-feedback/SKILL.md +1 -1
- package/skills/codex/verifying-mcp-delivery/SKILL.md +1 -1
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
This curated example owns the customer/end-user authentication capability slot. It proves that a SaaS app
|
|
4
4
|
can protect an MCP endpoint with direct OIDC, retain role/scope-based tool authorization, and route ordinary
|
|
5
|
-
|
|
5
|
+
reads and confirmed actions to the API origin selected by the verified customer's identity provider.
|
|
6
6
|
|
|
7
7
|
It also owns the customer-branded embedded-assistant presentation showcase. Embedded sessions lack
|
|
8
8
|
direct/federated MCP OIDC endpoint claims, so these routed tools fail with `connector_route_unavailable`;
|
|
9
9
|
static connectors still support embedded delegated exchange. Exercise routed tools through the MCP endpoint.
|
|
10
10
|
|
|
11
11
|
The public developer entrypoint is [`src/server.ts`](src/server.ts). It exposes a deliberately small MCP
|
|
12
|
-
surface for organization discovery:
|
|
12
|
+
surface for organization discovery and app lifecycle operations:
|
|
13
13
|
|
|
14
14
|
- `list_my_organizations` lists the NoodleSeed.com organizations the signed-in customer belongs to (no
|
|
15
15
|
arguments — the org set comes from the verified customer session).
|
|
16
16
|
- `list_org_apps` lists apps for one of those organizations through that tenant's API. It is visible and
|
|
17
17
|
callable only when the verified customer has the `org_apps:read` scope and either the `org_admin` or
|
|
18
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.
|
|
19
21
|
|
|
20
|
-
The
|
|
21
|
-
`list_org_apps` takes one of those `
|
|
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.
|
|
22
25
|
|
|
23
26
|
## Declare the customer endpoint
|
|
24
27
|
|
|
@@ -48,11 +51,11 @@ const api = connector('noodleseed_app_api')
|
|
|
48
51
|
tokenUrl: 'https://id.noodleseed.dev/oauth/token',
|
|
49
52
|
clientId: variable('CUSTOMER_API_CLIENT_ID'),
|
|
50
53
|
clientSecret: secret('CUSTOMER_API_CLIENT_SECRET'),
|
|
51
|
-
scopes: ['organizations:read', 'org_apps:read'],
|
|
54
|
+
scopes: ['organizations:read', 'org_apps:read', 'org_apps:write'],
|
|
52
55
|
audience: 'noodleseed-customer-api',
|
|
53
56
|
},
|
|
54
57
|
operations: {
|
|
55
|
-
// read operations...
|
|
58
|
+
// read and action operations...
|
|
56
59
|
},
|
|
57
60
|
});
|
|
58
61
|
```
|
|
@@ -65,7 +68,7 @@ public `${user}` expression scope:
|
|
|
65
68
|
```ts
|
|
66
69
|
auth: customerAuth.oidc({
|
|
67
70
|
issuer: 'https://id.noodleseed.dev',
|
|
68
|
-
audience: '
|
|
71
|
+
audience: 'noodleseed-customer-auth-prod',
|
|
69
72
|
claims: {
|
|
70
73
|
id: 'sub',
|
|
71
74
|
email: 'email',
|
|
@@ -90,7 +93,7 @@ auth: customerAuth.federatedOidc({
|
|
|
90
93
|
issuers: [
|
|
91
94
|
{
|
|
92
95
|
issuer: 'https://id.customer-a.com',
|
|
93
|
-
audience: '
|
|
96
|
+
audience: 'noodleseed-customer-auth-prod',
|
|
94
97
|
routing: {
|
|
95
98
|
endpoints: {
|
|
96
99
|
customer_api: { claim: 'tenant.api_base_url' },
|
|
@@ -99,7 +102,7 @@ auth: customerAuth.federatedOidc({
|
|
|
99
102
|
},
|
|
100
103
|
{
|
|
101
104
|
issuer: 'https://login.customer-b.com',
|
|
102
|
-
audience: '
|
|
105
|
+
audience: 'noodleseed-customer-auth-prod',
|
|
103
106
|
routing: {
|
|
104
107
|
endpoints: {
|
|
105
108
|
customer_api: { claim: 'organization.routes.customer_api' },
|
|
@@ -110,21 +113,56 @@ auth: customerAuth.federatedOidc({
|
|
|
110
113
|
}),
|
|
111
114
|
```
|
|
112
115
|
|
|
113
|
-
At runtime, Noodle Seed validates
|
|
114
|
-
into private request state, applies its policy, and
|
|
116
|
+
At runtime, Noodle Seed validates the configured stable audience, associates the caller with the exact
|
|
117
|
+
transport-derived MCP resource, projects the route into private request state, applies its policy, and
|
|
118
|
+
freezes it for the call. Missing, malformed, or
|
|
115
119
|
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,
|
|
117
|
-
keys, or delegated exchange assertions.
|
|
120
|
+
Resolved URLs never enter artifacts, `${user}`, logs, model output, widgets, public confirmation review,
|
|
121
|
+
broker cache keys, or delegated exchange assertions.
|
|
118
122
|
|
|
119
|
-
|
|
120
|
-
`
|
|
121
|
-
`customer_endpoint_surface_unsupported`.
|
|
123
|
+
Routed reads work in tools, including declared nested calls. Routed actions require exact
|
|
124
|
+
`annotations.confirm: true`; otherwise they fail with `customer_endpoint_action_unsupported`. Routed
|
|
125
|
+
resources, prompts, and ambient context fail with `customer_endpoint_surface_unsupported`.
|
|
126
|
+
|
|
127
|
+
The flagship's routed action uses the normal TypeScript action helper:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
tool('archive_org_app', {
|
|
131
|
+
authorization: {
|
|
132
|
+
requiredScopes: ['org_apps:write'],
|
|
133
|
+
allowedRoles: ['org_admin'],
|
|
134
|
+
},
|
|
135
|
+
annotations: annotations.openAction({ destructive: false, confirm: true }),
|
|
136
|
+
// input, output, and the normal connectors.app_api.archiveOrgApp(...) call...
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
The flagship also opts into the current stateless hosted MCP path:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
interactions: {
|
|
144
|
+
confirmationFallback: 'host',
|
|
145
|
+
},
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
A bidirectional client that negotiated form elicitation can complete the standard confirmation exchange
|
|
149
|
+
instead. The explicit host fallback trusts the MCP host to have collected native write approval before the
|
|
150
|
+
tool call reaches Noodle Seed; it is never inferred from client identity and does not replace auth, policy,
|
|
151
|
+
or accurate action/destructive annotations. Omit the fallback when connected hosts are not trusted to
|
|
152
|
+
provide that approval. If neither standard confirmation nor the fallback is available, the action fails
|
|
153
|
+
closed with `interaction_unavailable`.
|
|
154
|
+
|
|
155
|
+
Preparation stores only sorted route `{ key, fingerprint }` bindings in its private server-held
|
|
156
|
+
continuation; the public review exposes none of them. Acceptance re-resolves the current request route and
|
|
157
|
+
returns `invalid_continuation` if it is missing or changed, before policy, credentials, or egress. A match
|
|
158
|
+
reuses the current frozen snapshot for the action and all nested or later reads.
|
|
122
159
|
|
|
123
160
|
The application developer owns the direct/federated authorization server. It must publish its path-inserted
|
|
124
161
|
RFC 8414 document as direct HTTP 200 JSON with exact issuer and HTTPS authorization/token/registration/JWKS
|
|
125
162
|
endpoints, authorization-code and refresh grants, PKCE S256, public-client auth method `none`, RFC 8707
|
|
126
|
-
resource handling, and public signing keys.
|
|
127
|
-
|
|
163
|
+
resource handling, and public signing keys. It validates each exact MCP resource on authorize, code exchange,
|
|
164
|
+
and refresh, then maps approved versions of this app/environment to `noodleseed-customer-auth-prod`. Other
|
|
165
|
+
apps and environments use distinct audiences.
|
|
128
166
|
|
|
129
167
|
Run `noodle auth doctor src/server.ts` before sharing. Its bounded, read-only probes never register a client.
|
|
130
168
|
Adding the embedded assistant does not choose or rewrite MCP customer auth.
|
|
@@ -180,7 +218,8 @@ noodle validate examples/customer-auth/src/server.ts
|
|
|
180
218
|
|
|
181
219
|
Against a deployed customer-protected environment, set a short-lived real customer token only in
|
|
182
220
|
`NOODLE_CUSTOMER_TOKEN` and add `--live --org <org> --app <app> --env <env>`. The live doctor performs
|
|
183
|
-
credential exchanges without invoking
|
|
221
|
+
credential exchanges without invoking any business tool. Add `--version 1` when testing a pinned version;
|
|
222
|
+
the reported customer resource must match that versioned MCP endpoint.
|
|
184
223
|
|
|
185
224
|
## Run locally
|
|
186
225
|
|
|
@@ -487,13 +526,16 @@ https://cloud.noodleseed.dev/o/noodleseed/customer-auth/mcp
|
|
|
487
526
|
- Tool `list_my_organizations`: calls `GET /api/organizations` and returns the organizations the signed-in
|
|
488
527
|
customer is a member of. Takes no arguments; the org set is scoped by the verified customer session.
|
|
489
528
|
- Tool `list_org_apps`: calls `GET /api/organizations/{org_id}/apps` for one organization `org_id`.
|
|
529
|
+
- Tool `archive_org_app`: after confirmation, calls
|
|
530
|
+
`POST /api/organizations/{org_id}/apps/{app_id}/archive`.
|
|
490
531
|
|
|
491
532
|
## Auth boundary
|
|
492
533
|
|
|
493
|
-
Noodle Seed verifies the configured OIDC issuer
|
|
494
|
-
routing claims. Public caller identity contains the user/role/scope
|
|
495
|
-
private request state.
|
|
534
|
+
Noodle Seed verifies the configured OIDC issuer and stable audience, then binds the exact transport-derived
|
|
535
|
+
MCP resource before reading identity or routing claims. Public caller identity contains the user/role/scope
|
|
536
|
+
projection; the customer route remains private request state.
|
|
496
537
|
|
|
497
538
|
Connector-backed tools ask the broker for a route-bound delegated credential; only the endpoint key and
|
|
498
539
|
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.
|
|
540
|
+
token never reach tools, connectors, widgets, model output, or downstream systems. Confirmed actions keep
|
|
541
|
+
the same URL-blind binding only in private continuation state and reject acceptance-time drift.
|
|
@@ -25,7 +25,7 @@ const noodleseedApi = connector('noodleseed_app_api')
|
|
|
25
25
|
tokenUrl: 'https://id.noodleseed.dev/oauth/token',
|
|
26
26
|
clientId: variable('CUSTOMER_API_CLIENT_ID'),
|
|
27
27
|
clientSecret: secret('CUSTOMER_API_CLIENT_SECRET'),
|
|
28
|
-
scopes: ['organizations:read', 'org_apps:read'],
|
|
28
|
+
scopes: ['organizations:read', 'org_apps:read', 'org_apps:write'],
|
|
29
29
|
audience: 'noodleseed-customer-api',
|
|
30
30
|
},
|
|
31
31
|
operations: {
|
|
@@ -53,6 +53,19 @@ const noodleseedApi = connector('noodleseed_app_api')
|
|
|
53
53
|
organizations: '${response.organizations}',
|
|
54
54
|
},
|
|
55
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
|
+
},
|
|
56
69
|
},
|
|
57
70
|
});
|
|
58
71
|
|
|
@@ -73,9 +86,10 @@ export default server(
|
|
|
73
86
|
},
|
|
74
87
|
},
|
|
75
88
|
use: { app_api: noodleseedApi },
|
|
89
|
+
interactions: { confirmationFallback: 'host' },
|
|
76
90
|
auth: customerAuth.oidc({
|
|
77
91
|
issuer: 'https://id.noodleseed.dev',
|
|
78
|
-
audience: '
|
|
92
|
+
audience: 'noodleseed-customer-auth-prod',
|
|
79
93
|
claims: {
|
|
80
94
|
id: 'sub',
|
|
81
95
|
email: 'email',
|
|
@@ -91,7 +105,7 @@ export default server(
|
|
|
91
105
|
},
|
|
92
106
|
}),
|
|
93
107
|
instructions:
|
|
94
|
-
'Direct/federated MCP OIDC demo. The customer IdP proves identity and privately selects the tenant API base URL
|
|
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.',
|
|
95
109
|
assistant: embeddedAssistant({
|
|
96
110
|
model: openAICompatible({
|
|
97
111
|
baseUrl: variable('ASSISTANT_MODEL_BASE_URL'),
|
|
@@ -169,5 +183,30 @@ export default server(
|
|
|
169
183
|
};
|
|
170
184
|
},
|
|
171
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
|
+
}),
|
|
172
211
|
],
|
|
173
212
|
);
|
|
@@ -26,7 +26,7 @@ describe('customer-auth example', () => {
|
|
|
26
26
|
expect(manifest.server.auth).toEqual({
|
|
27
27
|
kind: 'oidc',
|
|
28
28
|
issuer: 'https://id.noodleseed.dev',
|
|
29
|
-
audience: '
|
|
29
|
+
audience: 'noodleseed-customer-auth-prod',
|
|
30
30
|
claims: {
|
|
31
31
|
id: 'sub',
|
|
32
32
|
email: 'email',
|
|
@@ -41,6 +41,7 @@ describe('customer-auth example', () => {
|
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
43
|
});
|
|
44
|
+
expect(manifest.server.interactions).toEqual({ confirmationFallback: 'host' });
|
|
44
45
|
const catalog = app.toConnectorCatalog();
|
|
45
46
|
expect(catalog?.connectors).toHaveLength(1);
|
|
46
47
|
expect(catalog?.connectors[0]?.http).toMatchObject({
|
|
@@ -56,11 +57,11 @@ describe('customer-auth example', () => {
|
|
|
56
57
|
clientSecret: 'CUSTOMER_API_CLIENT_SECRET',
|
|
57
58
|
},
|
|
58
59
|
});
|
|
59
|
-
expect(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
)
|
|
60
|
+
expect(catalog?.connectors[0]?.operations).toMatchObject({
|
|
61
|
+
list_org_apps: { type: 'read' },
|
|
62
|
+
list_organizations: { type: 'read' },
|
|
63
|
+
archive_org_app: { type: 'action', method: 'POST' },
|
|
64
|
+
});
|
|
64
65
|
expect(catalog?.connectors[0]?.http).not.toHaveProperty('allowedOrigins');
|
|
65
66
|
expect(JSON.stringify({ manifest, catalog })).not.toContain('tenant-a.api.noodleseed.dev');
|
|
66
67
|
expect(manifest.tools.find((tool) => tool.name === 'list_org_apps')?.authorization).toEqual({
|
|
@@ -70,5 +71,17 @@ describe('customer-auth example', () => {
|
|
|
70
71
|
expect(
|
|
71
72
|
manifest.tools.find((tool) => tool.name === 'list_my_organizations')?.authorization,
|
|
72
73
|
).toBeUndefined();
|
|
74
|
+
expect(manifest.tools.find((tool) => tool.name === 'archive_org_app')).toMatchObject({
|
|
75
|
+
authorization: {
|
|
76
|
+
requiredScopes: ['org_apps:write'],
|
|
77
|
+
allowedRoles: ['org_admin'],
|
|
78
|
+
},
|
|
79
|
+
annotations: {
|
|
80
|
+
readOnlyHint: false,
|
|
81
|
+
destructiveHint: false,
|
|
82
|
+
openWorldHint: true,
|
|
83
|
+
confirm: true,
|
|
84
|
+
},
|
|
85
|
+
});
|
|
73
86
|
});
|
|
74
87
|
});
|
|
@@ -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.56.1 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.56.1 hash:efffbf82007f935d -->
|
|
7
7
|
|
|
8
8
|
# publishing-mcp-integrations
|
|
9
9
|
|
|
@@ -108,14 +108,14 @@ More: `auth.kind` is `bearer` | `apiKey` (needs `header`) | `clientCredentials`
|
|
|
108
108
|
|
|
109
109
|
For `customerAuth.oidc(...)` and `.federatedOidc(...)`, the application developer owns the standards-compliant authorization server. Noodle verifies its access tokens; it does not proxy discovery, create OAuth clients, or repair the upstream server. For issuer `https://id.example.com/oauth`, publish the path-inserted RFC 8414 document at `https://id.example.com/.well-known/oauth-authorization-server/oauth` as direct unauthenticated HTTP 200 JSON — never a login redirect.
|
|
110
110
|
|
|
111
|
-
That metadata must expose HTTPS `authorization_endpoint`, `token_endpoint`, `jwks_uri`, and RFC 7591 `registration_endpoint`; advertise authorization-code and refresh-token grants, Dynamic Client Registration, PKCE with `code_challenge_methods_supported: ["S256"]`, and public clients with `token_endpoint_auth_methods_supported: ["none"]`.
|
|
111
|
+
That metadata must expose HTTPS `authorization_endpoint`, `token_endpoint`, `jwks_uri`, and RFC 7591 `registration_endpoint`; advertise authorization-code and refresh-token grants, Dynamic Client Registration, PKCE with `code_challenge_methods_supported: ["S256"]`, and public clients with `token_endpoint_auth_methods_supported: ["none"]`. Validate the exact RFC 8707 `resource` on authorize, code exchange, and refresh, then map approved versions of one app/environment to the stable audience configured in `customerAuth`; use distinct audiences across apps and environments. Publish only public signing keys in JWKS. Run `noodle auth doctor src/server.ts`; its issuer-readiness probes perform bounded read-only GET checks and never register a client. A successful `noodle deploy --access customers` reports the same readiness without turning a diagnostic failure into a failed deployment.
|
|
112
112
|
|
|
113
113
|
## Auth-derived customer API endpoints
|
|
114
114
|
|
|
115
115
|
Use a customer endpoint when the verified IdP selects a different API base URL for each SaaS customer. The claim contains the complete base URL; tool input and `${user}` do not select it. Declare one named policy, use that reference as the connector `baseUrl`, and map its claim path in direct OIDC or on every federated issuer:
|
|
116
116
|
|
|
117
117
|
```ts
|
|
118
|
-
import { connector, customerAuth, customerEndpoint, secret, server, tool, variable, z } from '@noodleseed/one';
|
|
118
|
+
import { annotations, connector, customerAuth, customerEndpoint, secret, server, tool, variable, z } from '@noodleseed/one';
|
|
119
119
|
|
|
120
120
|
const customerApi = customerEndpoint('customer_api', {
|
|
121
121
|
allowedHttpsHostSuffixes: ['api.noodleseed.dev'],
|
|
@@ -137,6 +137,13 @@ const api = connector('customer_api_connector').version('1.0.0').http({
|
|
|
137
137
|
input: z.object({}),
|
|
138
138
|
output: z.object({ records: z.array(z.unknown()).max(100) }),
|
|
139
139
|
},
|
|
140
|
+
archive_record: {
|
|
141
|
+
type: 'action',
|
|
142
|
+
method: 'POST',
|
|
143
|
+
path: '/records/${args.record_id}/archive',
|
|
144
|
+
input: z.object({ record_id: z.string() }),
|
|
145
|
+
output: z.object({ archived: z.boolean() }),
|
|
146
|
+
},
|
|
140
147
|
},
|
|
141
148
|
});
|
|
142
149
|
|
|
@@ -146,9 +153,10 @@ export default server(
|
|
|
146
153
|
title: 'Customer records',
|
|
147
154
|
version: '1.0.0',
|
|
148
155
|
use: { api },
|
|
156
|
+
interactions: { confirmationFallback: 'host' },
|
|
149
157
|
auth: customerAuth.oidc({
|
|
150
158
|
issuer: 'https://id.noodleseed.dev',
|
|
151
|
-
audience: '
|
|
159
|
+
audience: 'noodleseed-customer-records-prod',
|
|
152
160
|
routing: {
|
|
153
161
|
endpoints: {
|
|
154
162
|
customer_api: { claim: 'tenant.api_base_url' },
|
|
@@ -166,17 +174,29 @@ export default server(
|
|
|
166
174
|
return { records: result.records };
|
|
167
175
|
},
|
|
168
176
|
}),
|
|
177
|
+
tool('archive_record', {
|
|
178
|
+
description: 'Archive one record for the signed-in customer.',
|
|
179
|
+
input: z.object({ record_id: z.string() }),
|
|
180
|
+
output: z.object({ archived: z.boolean() }),
|
|
181
|
+
annotations: annotations.openAction({ destructive: false, confirm: true }),
|
|
182
|
+
fulfil({ input, connectors }) {
|
|
183
|
+
const result = connectors.api.archiveRecord({ record_id: input.record_id });
|
|
184
|
+
return { archived: result.archived };
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
169
187
|
],
|
|
170
188
|
);
|
|
171
189
|
```
|
|
172
190
|
|
|
173
191
|
`customerEndpoint` accepts exactly one non-empty policy arm: exact HTTPS origins, or HTTPS hostname suffixes. Exact policies may authorize an explicit non-default port; suffix policies allow port 443 and match only the exact host or dot-boundary subdomains. Do not add connector `allowedOrigins` to a customer-routed connector. Its fixed credential/token endpoints are validated independently and cannot come from caller claims.
|
|
174
192
|
|
|
193
|
+
On a bidirectional MCP transport whose client negotiated form elicitation, Noodle sends the standard confirmation form. The current stateless hosted MCP transport cannot initiate that exchange, so the example explicitly declares `server.interactions.confirmationFallback: 'host'`. That fallback trusts the MCP host to have collected native write approval before the tool call reaches Noodle; it is never inferred from client identity and does not replace authentication, authorization, policy, or accurate action/destructive annotations. Omit it when connected hosts are not trusted to provide that approval; confirmation then fails closed with `interaction_unavailable` when the standard exchange is unavailable.
|
|
194
|
+
|
|
175
195
|
Every federated issuer must repeat every endpoint key used by the app, although each issuer may choose a different claim path. Endpoint names use lowercase letters, numbers, and underscores. Resolved claims must be exact absolute HTTPS URLs of at most 2,048 UTF-8 bytes with no userinfo, query, fragment, IP literal, special-use host, or unsafe whitespace/control characters. The runtime preserves a canonical optional base path.
|
|
176
196
|
|
|
177
|
-
Resolved customer URLs are private routing authority: they never enter the manifest/artifact, `${user}`, logs, model output, widgets, confirmation review, cache keys, or delegated-token-exchange assertions. Routed
|
|
197
|
+
Resolved customer URLs are private routing authority: they never enter the manifest/artifact, `${user}`, logs, model output, widgets, public confirmation review, cache keys, or delegated-token-exchange assertions. Routed reads may be used by tools, including declared nested calls. A routed action—including one reached through a connector wrapper—requires exact `annotations.confirm: true`; omitted or `false` fails with `customer_endpoint_action_unsupported`. Routed resources, prompts, and ambient context fail with `customer_endpoint_surface_unsupported`.
|
|
178
198
|
|
|
179
|
-
At runtime,
|
|
199
|
+
At runtime, an initially missing, malformed, or disallowed claim returns the same safe `connector_route_unavailable` tool error before credential lookup or connector egress. Preparation stores only sorted route `{ key, fingerprint }` bindings in the private server-held continuation. Acceptance re-resolves the current request routes; a missing or changed binding returns `invalid_continuation` before policy, credentials, or egress, then the matching frozen snapshot is reused for the action and nested/later calls. `tools/list` remains based only on roles and scopes, so route availability neither reveals tenant topology nor changes the existing authorization filter.
|
|
180
200
|
|
|
181
201
|
## Per-tool authorization
|
|
182
202
|
|
|
@@ -267,7 +287,7 @@ export async function tokenEndpoint(req: Request): Promise<Response> {
|
|
|
267
287
|
}
|
|
268
288
|
```
|
|
269
289
|
|
|
270
|
-
Diagnose statically with `noodle auth doctor`; set a short-lived real customer token only in `NOODLE_CUSTOMER_TOKEN` and add `--live --org <org> --app <app> --env <env>` to perform one exchange per delegated binding without invoking a business tool. Common failures include structured `credential_unavailable` reasons such as `caller_identity_not_customer`. Direct/federated OIDC verification assigns the customer identity at the trusted verifier boundary; never ask an IdP to mint a Noodle-specific classification claim.
|
|
290
|
+
Diagnose statically with `noodle auth doctor`; set a short-lived real customer token only in `NOODLE_CUSTOMER_TOKEN` and add `--live --org <org> --app <app> --env <env>` to perform one exchange per delegated binding without invoking a business tool. Add `--version <version>` to test that exact pinned MCP resource. Common failures include structured `credential_unavailable` reasons such as `caller_identity_not_customer`. Direct/federated OIDC verification assigns the customer identity at the trusted verifier boundary; never ask an IdP to mint a Noodle-specific classification claim.
|
|
271
291
|
|
|
272
292
|
## Design tools for the model
|
|
273
293
|
|
|
@@ -67,9 +67,10 @@ Run `noodle validate` (add `--json` for the machine-readable envelope, `--fix-pr
|
|
|
67
67
|
| `customer_endpoint_mapping_required` | Add the endpoint key at the cited auth routing path; every direct/federated issuer must map every customer endpoint used by the app. |
|
|
68
68
|
| `customer_endpoint_unknown_mapping` | Remove the unknown or unused auth routing key, or use that exact declared `customerEndpoint` key from a reachable connector operation. |
|
|
69
69
|
| `customer_endpoint_bridge_unsupported` | Replace the Firebase/Microsoft bridge with direct or federated OIDC before using auth-derived customer connector endpoints. |
|
|
70
|
-
| `customer_endpoint_action_unsupported` |
|
|
70
|
+
| `customer_endpoint_action_unsupported` | Set exact `annotations.confirm: true` on the enclosing tool, or keep the customer-routed operation read-only; action hints alone do not enable confirmation. |
|
|
71
71
|
| `customer_endpoint_surface_unsupported` | Move the customer-routed call into a tool fulfilment; routed resources, prompts, and ambient providers are unsupported. |
|
|
72
72
|
| `customer_endpoint_credential_source_unsupported` | Remove the manifest connection binding; a customer-routed connector uses its declared delegated token exchange auth or no auth. |
|
|
73
73
|
| `customer_endpoint_policy_conflict` | Give every reachable declaration of this endpoint key one identical policy, or rename keys whose allowed origins differ. |
|
|
74
|
+
| `customer_endpoint_routing_inconsistent` | Regenerate the connector catalog so every action route includes all of its ordinary customer endpoint dependencies. |
|
|
74
75
|
| `unused_connector_alias` | A declared connector alias is never called; remove the unused `use` entry or wire it into a tool. |
|
|
75
76
|
| `arg_mismatch` | A connector call is missing or adds arguments; match the operation signature under `expected`/`got`. |
|
|
@@ -14,7 +14,7 @@ Paths are relative to this skill directory. Assets (images/fonts) are omitted fr
|
|
|
14
14
|
| `acme-discovery` | Top-of-funnel discovery→handoff: a discovery carousel, a `create_handoff` deep link, and a design-first UX spec + wireframe. | `examples/acme-discovery/src/server.ts` + `design/` |
|
|
15
15
|
| `acme-tasks` | A two-way productivity app designed around its top-3 prioritized flows (capture/prioritize/complete), with a design-first flow spec + wireframe. | `examples/acme-tasks/src/server.ts` + `design/` |
|
|
16
16
|
| `acme-bistro` | End-to-end ordering with a payment-only handoff; ships a gold-standard `design/` set (UX doc, wireframe with compliance audit, API contract). | `examples/acme-bistro/src/server.ts` + `design/` |
|
|
17
|
-
| `customer-auth` | End-user
|
|
17
|
+
| `customer-auth` | End-user OIDC, private customer API routing, route-bound confirmed actions, roles/scopes, and delegated credentials. | `examples/customer-auth/src/server.ts` |
|
|
18
18
|
| `gmail-multi-account` | One curated Gmail connector reused by two account bindings, canonical account arrays, exact mutation confirmation, and an accompanying personal-automation skill. | `examples/gmail-multi-account/src/server.ts` |
|
|
19
19
|
| `google-bigquery` | Keyless Google Workload Identity Federation with optional service-account impersonation, a BigQuery REST connector, and complete developer/operator setup. | `examples/google-bigquery/src/server.ts` |
|
|
20
20
|
|
|
@@ -3,7 +3,7 @@ name: reporting-noodle-feedback
|
|
|
3
3
|
description: "Use when a Noodle Seed bug, misleading instruction, missing capability, or concrete product improvement should be proposed to the user."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.56.1 hash:0f404109f4845683 -->
|
|
7
7
|
|
|
8
8
|
# reporting-noodle-feedback
|
|
9
9
|
|
|
@@ -3,7 +3,7 @@ name: verifying-mcp-delivery
|
|
|
3
3
|
description: "Use when proving a Noodle Seed MCP project works at a named compile, local, connector, App, host, deployment, or production evidence level."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
<!-- noodle-skill version:0.
|
|
6
|
+
<!-- noodle-skill version:0.56.1 hash:6ef6ef551e26b78e -->
|
|
7
7
|
|
|
8
8
|
# verifying-mcp-delivery
|
|
9
9
|
|