zuplo 6.71.27 → 6.72.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/docs/analytics/access-and-entitlements.md +7 -8
- package/docs/analytics/overview.md +5 -5
- package/docs/analytics/shared-controls.md +2 -2
- package/docs/articles/securing-backend-mtls.mdx +3 -0
- package/docs/dev-portal/zudoku/components/landing-page.mdx +1 -1
- package/docs/policies/_index.md +1 -1
- package/docs/policies/mcp-capability-filter-inbound/doc.md +132 -0
- package/docs/policies/mcp-capability-filter-inbound/schema.json +138 -1
- package/docs/policies/mcp-token-exchange-inbound/doc.md +41 -3
- package/docs/policies/mcp-token-exchange-inbound/schema.json +13 -0
- package/package.json +4 -4
|
@@ -5,23 +5,22 @@ sidebar_label: "Access & Entitlements"
|
|
|
5
5
|
|
|
6
6
|
## When to use this
|
|
7
7
|
|
|
8
|
-
- Confirm whether your account can see
|
|
8
|
+
- Confirm whether your account can see analytics.
|
|
9
9
|
- Find out how many days of history you have access to.
|
|
10
10
|
- Understand the trial banner or the demo mode link.
|
|
11
11
|
|
|
12
12
|
## Plan requirements
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
Analytics must be enabled on your account. Without it, the Analytics page shows
|
|
15
|
+
an upsell view with a **Contact Sales** call-to-action and no charts.
|
|
16
16
|
|
|
17
17
|
## Free trial
|
|
18
18
|
|
|
19
|
-
New accounts with
|
|
20
|
-
trial:
|
|
19
|
+
New accounts with analytics enabled get an automatic free trial. The trial:
|
|
21
20
|
|
|
22
21
|
- Runs for the same number of days as your account's retention window.
|
|
23
22
|
- Shows a banner across the top of the Analytics page: "You're on a {N}-day
|
|
24
|
-
preview of
|
|
23
|
+
preview of Analytics, {N} days left."
|
|
25
24
|
- Includes two call-to-actions: **View demo →** (loads the dashboard with sample
|
|
26
25
|
data) and **Contact sales**.
|
|
27
26
|
|
|
@@ -54,8 +53,8 @@ Append `?demo=true` to the Analytics URL, or click **View demo →** in the tria
|
|
|
54
53
|
banner, to switch into demo mode. In demo mode:
|
|
55
54
|
|
|
56
55
|
- Charts and tables are populated with synthetic sample data.
|
|
57
|
-
- A persistent banner reads: "You're viewing the
|
|
58
|
-
|
|
56
|
+
- A persistent banner reads: "You're viewing the Analytics demo with sample
|
|
57
|
+
data. Your real analytics aren't shown here."
|
|
59
58
|
|
|
60
59
|
Remove the `demo` parameter from the URL to return to your real data.
|
|
61
60
|
|
|
@@ -56,12 +56,12 @@ You'll see a subset of sections depending on your plan and project setup:
|
|
|
56
56
|
|
|
57
57
|
| Section | When it appears |
|
|
58
58
|
| --------- | ---------------------------------------------------------- |
|
|
59
|
-
| Requests | All accounts with
|
|
59
|
+
| Requests | All accounts with analytics enabled. |
|
|
60
60
|
| Origins | The project uses managed-edge origins. |
|
|
61
|
-
| Consumers | All accounts with
|
|
62
|
-
| Agents | All accounts with
|
|
61
|
+
| Consumers | All accounts with analytics enabled. |
|
|
62
|
+
| Agents | All accounts with analytics enabled. |
|
|
63
63
|
| MCP | The project type is **standard** and the project uses MCP. |
|
|
64
64
|
| GraphQL | The project proxies a GraphQL API. |
|
|
65
65
|
|
|
66
|
-
If you don't see Analytics at all, your account likely doesn't have
|
|
67
|
-
|
|
66
|
+
If you don't see Analytics at all, your account likely doesn't have analytics
|
|
67
|
+
enabled. See [Access and entitlements](./access-and-entitlements.md).
|
|
@@ -99,8 +99,8 @@ Banners appear at the top of the page in this priority order:
|
|
|
99
99
|
1. **Preview banner**: when `preview=1` is set. Indicates legacy preview mode.
|
|
100
100
|
2. **Demo banner**: when `demo=true` is set. Reminds you sample data is shown
|
|
101
101
|
instead of your real analytics.
|
|
102
|
-
3. **Trial banner**: for new accounts with
|
|
103
|
-
|
|
102
|
+
3. **Trial banner**: for new accounts with analytics. Shows days remaining and
|
|
103
|
+
offers **View demo →** and **Contact Sales**.
|
|
104
104
|
|
|
105
105
|
## Loading and empty states
|
|
106
106
|
|
|
@@ -263,6 +263,9 @@ For more information on securing your backend, see:
|
|
|
263
263
|
Alternative approach using shared secrets
|
|
264
264
|
- [Secure Tunnels](./secure-tunnel.mdx) - Connect to private backends without
|
|
265
265
|
exposing them to the internet
|
|
266
|
+
- [Client mTLS Authentication](./securing-the-gateway-with-client-mtls.mdx) -
|
|
267
|
+
The reverse direction, where clients authenticate to your Zuplo gateway with a
|
|
268
|
+
client certificate
|
|
266
269
|
|
|
267
270
|
If you need assistance configuring mTLS for your project, contact us at
|
|
268
271
|
[support@zuplo.com](mailto:support@zuplo.com).
|
|
@@ -10,7 +10,7 @@ A ready-made, customizable landing page for your developer portal. It comes in t
|
|
|
10
10
|
covering the most common layouts: a centered hero, a two-column split hero, and a compact
|
|
11
11
|
documentation hub.
|
|
12
12
|
|
|
13
|
-
Use it as the `element` of a [custom page](/dev-portal/zudoku/custom-pages) — typically your home page:
|
|
13
|
+
Use it as the `element` of a [custom page](/dev-portal/zudoku/guides/custom-pages) — typically your home page:
|
|
14
14
|
|
|
15
15
|
```tsx title=zudoku.config.tsx
|
|
16
16
|
import { LandingPage } from "zudoku/components";
|
package/docs/policies/_index.md
CHANGED
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
| ldap-auth-inbound | LDAP Auth | Authenticate requests using an LDAP server. | api-gateway |
|
|
58
58
|
| mcp-cognito-oauth-inbound | MCP Amazon Cognito OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Amazon Cognito. Cognito-friendly wrapper around `McpOAuthInboundPolicy`. Provide an AWS region, user pool id, user pool domain, client id, and client secret; the constructor derives the Cognito issuer, JWKS URL, authorize URL, and token URL. | mcp-gateway |
|
|
59
59
|
| mcp-auth0-oauth-inbound | MCP Auth0 OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Auth0. Auth0-friendly wrapper around `McpOAuthInboundPolicy`. Provide `auth0Domain` and `clientId`; the constructor derives the OIDC issuer, JWKS URL, and Auth0 authorize/token endpoints automatically and runs the resulting shape through the same Zod schema as the generic policy. Validation runs lazily inside the policy constructor, which the runtime caches per policy name — so a misconfigured policy fails the first request with a `ConfigurationError` (surfaced in the 500 problem body) rather than crashing boot. | mcp-gateway |
|
|
60
|
-
| mcp-capability-filter-inbound | MCP Capability Filter | Curate MCP capabilities advertised and reachable through `McpProxyHandler`. Validation runs lazily inside the policy constructor, which the runtime caches per policy name. Misconfigured options therefore fail on first use with a customer-facing `ConfigurationError` instead of failing at module load. | mcp-gateway |
|
|
60
|
+
| mcp-capability-filter-inbound | MCP Capability Filter | Curate MCP capabilities advertised and reachable through `McpProxyHandler`. Validation runs lazily inside the policy constructor, which the runtime caches per policy name. Misconfigured options therefore fail on first use with a customer-facing `ConfigurationError` instead of failing at module load. Optional per-caller access control (`accessControl`) narrows the curated catalog. See `claimsCapabilityResolver` for built-in role/group matching and `McpCapabilityResolver` for custom resolvers. | mcp-gateway |
|
|
61
61
|
| mcp-clerk-oauth-inbound | MCP Clerk OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Clerk. Clerk-friendly wrapper around `McpOAuthInboundPolicy`. Provide Clerk's Frontend API URL plus the OAuth application client id and secret; the constructor derives the Clerk issuer, JWKS URL, authorize URL, and token URL. | mcp-gateway |
|
|
62
62
|
| mcp-google-oauth-inbound | MCP Google OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Google. Google-friendly wrapper around `McpOAuthInboundPolicy`. Provide `clientId` and `clientSecret`; the constructor uses Google's fixed OIDC issuer, JWKS URL, authorize URL, and token URL, then runs the resulting shape through the same Zod schema as the generic policy. | mcp-gateway |
|
|
63
63
|
| mcp-keycloak-oauth-inbound | MCP Keycloak OAuth | Authenticate MCP gateway requests using a gateway-issued OAuth access token, with browser login delegated to Keycloak. Keycloak-friendly wrapper around `McpOAuthInboundPolicy`. Provide the Keycloak server root, realm, client id, and client secret; the constructor derives the realm issuer, JWKS URL, authorize URL, and token URL from Keycloak's documented OIDC endpoint layout. | mcp-gateway |
|
|
@@ -50,6 +50,138 @@ gateway-managed upstream OAuth credentials. That order lets the token exchange
|
|
|
50
50
|
policy retry or replace a 401 response first; this policy then filters the final
|
|
51
51
|
upstream JSON-RPC response.
|
|
52
52
|
|
|
53
|
+
## Access control
|
|
54
|
+
|
|
55
|
+
`accessControl` narrows the curated catalog **per caller**. Its `mode` selects
|
|
56
|
+
the strategy:
|
|
57
|
+
|
|
58
|
+
- **`allPublic`** (default): no access control — every curated capability is
|
|
59
|
+
available to all callers. Setting `roles`/`groups`/`public` on a capability in
|
|
60
|
+
this mode is a configuration error (it would silently do nothing).
|
|
61
|
+
- **`rolesAndGroups`**: match the caller's roles and groups — read from
|
|
62
|
+
`request.user.data` (see below) — against each capability's markup.
|
|
63
|
+
- **`function`**: delegate to a custom resolver.
|
|
64
|
+
|
|
65
|
+
### Mode `rolesAndGroups`
|
|
66
|
+
|
|
67
|
+
Mark each capability with `roles` and/or `groups`, or `public: true`. **Every
|
|
68
|
+
capability must be classified** — one that sets neither (or both) is rejected at
|
|
69
|
+
load, so nothing is ever exposed by omission:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"options": {
|
|
74
|
+
"tools": [
|
|
75
|
+
{ "name": "search_invoices", "roles": ["billing", "admin"] },
|
|
76
|
+
{ "name": "create_invoice", "roles": ["admin"], "groups": ["finance"] },
|
|
77
|
+
{ "name": "list_products", "public": true }
|
|
78
|
+
],
|
|
79
|
+
"accessControl": { "mode": "rolesAndGroups" }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
A caller may use a capability if it is `public`, or if the caller matches any
|
|
85
|
+
listed role **or** any listed group (so `create_invoice` above is available to
|
|
86
|
+
admins or to the finance team). Anonymous callers get only `public`
|
|
87
|
+
capabilities. `roles`/`groups`/`public` are enforcement markup and are stripped
|
|
88
|
+
from downstream responses — use `_meta` for hints you want clients to see.
|
|
89
|
+
|
|
90
|
+
The caller's roles and groups come from `request.user.data` — the object an
|
|
91
|
+
upstream authentication policy sets for the authenticated caller. What lands
|
|
92
|
+
there depends on the policy: a JWT auth policy stores the token's claims, an API
|
|
93
|
+
key policy stores the key's metadata, and so on. This policy reads the `roles`
|
|
94
|
+
and `groups` properties by default. For example, behind a JWT auth policy a
|
|
95
|
+
token with these claims:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"sub": "user_8f3a",
|
|
100
|
+
"roles": ["billing"],
|
|
101
|
+
"groups": ["sales"],
|
|
102
|
+
"iss": "https://acme.us.auth0.com/",
|
|
103
|
+
"aud": "https://mcp.acme.com",
|
|
104
|
+
"exp": 1893456000
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`request.user.data.roles` is `["billing"]` and `request.user.data.groups` is
|
|
109
|
+
`["sales"]`. Against the config above, this caller may use `search_invoices`
|
|
110
|
+
(matches the `billing` role) and the public `list_products`, but not
|
|
111
|
+
`create_invoice` (which needs the `admin` role or the `finance` group). A
|
|
112
|
+
request with no authenticated caller (no `request.user`) has no roles or groups,
|
|
113
|
+
so it sees only `public` capabilities.
|
|
114
|
+
|
|
115
|
+
Use `roleClaim`/`groupClaim` to read different `request.user.data` properties,
|
|
116
|
+
including a dot-path for nested values or a literal namespaced key:
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"accessControl": { "mode": "rolesAndGroups", "groupClaim": "cognito:groups" }
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"accessControl": {
|
|
127
|
+
"mode": "rolesAndGroups",
|
|
128
|
+
"roleClaim": "realm_access.roles"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Mode `function`
|
|
134
|
+
|
|
135
|
+
For any other mapping — looking up entitlements from an external API, requiring
|
|
136
|
+
both a role AND a group, blocklists, etc. — set `mode: "function"` and point
|
|
137
|
+
`identifier` at a resolver (same `{ module, export }` shape as rate-limit). **It
|
|
138
|
+
fully replaces the built-in roles/groups matching.** It receives
|
|
139
|
+
`(request, context, options)` and returns the allowed identifiers; the result is
|
|
140
|
+
always clamped to the configured catalog, so a resolver can only narrow access.
|
|
141
|
+
A capability type left as passthrough (no static list) is not narrowed. A
|
|
142
|
+
resolver that throws or returns no value fails closed.
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"accessControl": {
|
|
147
|
+
"mode": "function",
|
|
148
|
+
"identifier": {
|
|
149
|
+
"module": "$import(./modules/mcp-access-control)",
|
|
150
|
+
"export": "default"
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// modules/mcp-access-control.ts
|
|
158
|
+
import { ZuploRequest, ZuploContext, environment } from "@zuplo/runtime";
|
|
159
|
+
import type {
|
|
160
|
+
McpCapabilityFilterInboundPolicyOptions,
|
|
161
|
+
AllowedCapabilities,
|
|
162
|
+
} from "@zuplo/runtime/mcp-gateway";
|
|
163
|
+
|
|
164
|
+
export default async function resolveCapabilities(
|
|
165
|
+
request: ZuploRequest,
|
|
166
|
+
context: ZuploContext,
|
|
167
|
+
options: McpCapabilityFilterInboundPolicyOptions
|
|
168
|
+
): Promise<AllowedCapabilities> {
|
|
169
|
+
const subject = request.user?.sub;
|
|
170
|
+
if (!subject) return { tools: [] }; // fail closed for anonymous
|
|
171
|
+
|
|
172
|
+
const res = await fetch(
|
|
173
|
+
`https://entitlements.example.com/users/${encodeURIComponent(subject)}`,
|
|
174
|
+
{ headers: { authorization: `Bearer ${environment.ENTITLEMENTS_API_KEY}` } }
|
|
175
|
+
);
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
context.log.warn(`entitlements lookup failed: ${res.status}`);
|
|
178
|
+
return { tools: [] };
|
|
179
|
+
}
|
|
180
|
+
const { allowedTools } = (await res.json()) as { allowedTools: string[] };
|
|
181
|
+
return { tools: allowedTools };
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
53
185
|
## Batch Requests
|
|
54
186
|
|
|
55
187
|
For JSON-RPC batch requests, list responses are filtered per response item when
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"isHidden": false,
|
|
12
12
|
"requiresAI": false,
|
|
13
13
|
"products": ["mcp-gateway"],
|
|
14
|
-
"description": "Curate MCP capabilities advertised and reachable through `McpProxyHandler`.\n\nValidation runs lazily inside the policy constructor, which the runtime caches per policy name. Misconfigured options therefore fail on first use with a customer-facing `ConfigurationError` instead of failing at module load.",
|
|
14
|
+
"description": "Curate MCP capabilities advertised and reachable through `McpProxyHandler`.\n\nValidation runs lazily inside the policy constructor, which the runtime caches per policy name. Misconfigured options therefore fail on first use with a customer-facing `ConfigurationError` instead of failing at module load.\n\nOptional per-caller access control (`accessControl`) narrows the curated catalog. See `claimsCapabilityResolver` for built-in role/group matching and `McpCapabilityResolver` for custom resolvers.",
|
|
15
15
|
"deprecatedMessage": "",
|
|
16
16
|
"required": ["handler"],
|
|
17
17
|
"properties": {
|
|
@@ -67,6 +67,28 @@
|
|
|
67
67
|
"type": "object",
|
|
68
68
|
"additionalProperties": true,
|
|
69
69
|
"description": "Arbitrary MCP \\_meta fields to expose downstream."
|
|
70
|
+
},
|
|
71
|
+
"roles": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"minItems": 1,
|
|
74
|
+
"items": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"minLength": 1
|
|
77
|
+
},
|
|
78
|
+
"description": "Access control: roles allowed to see and invoke this capability. Matched against the caller's roles on request.user.data (accessControl.roleClaim, default 'roles'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
79
|
+
},
|
|
80
|
+
"groups": {
|
|
81
|
+
"type": "array",
|
|
82
|
+
"minItems": 1,
|
|
83
|
+
"items": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"minLength": 1
|
|
86
|
+
},
|
|
87
|
+
"description": "Access control: groups allowed to see and invoke this capability. Matched against the caller's groups on request.user.data (accessControl.groupClaim, default 'groups'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
88
|
+
},
|
|
89
|
+
"public": {
|
|
90
|
+
"type": "boolean",
|
|
91
|
+
"description": "Access control: mark this capability as available to all callers. In mode 'rolesAndGroups' every capability must set roles/groups OR public: true (not both) — an unclassified capability is a configuration error, so nothing is ever exposed by omission."
|
|
70
92
|
}
|
|
71
93
|
}
|
|
72
94
|
}
|
|
@@ -100,6 +122,28 @@
|
|
|
100
122
|
"type": "object",
|
|
101
123
|
"additionalProperties": true,
|
|
102
124
|
"description": "Arbitrary MCP \\_meta fields to expose downstream."
|
|
125
|
+
},
|
|
126
|
+
"roles": {
|
|
127
|
+
"type": "array",
|
|
128
|
+
"minItems": 1,
|
|
129
|
+
"items": {
|
|
130
|
+
"type": "string",
|
|
131
|
+
"minLength": 1
|
|
132
|
+
},
|
|
133
|
+
"description": "Access control: roles allowed to see and invoke this capability. Matched against the caller's roles on request.user.data (accessControl.roleClaim, default 'roles'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
134
|
+
},
|
|
135
|
+
"groups": {
|
|
136
|
+
"type": "array",
|
|
137
|
+
"minItems": 1,
|
|
138
|
+
"items": {
|
|
139
|
+
"type": "string",
|
|
140
|
+
"minLength": 1
|
|
141
|
+
},
|
|
142
|
+
"description": "Access control: groups allowed to see and invoke this capability. Matched against the caller's groups on request.user.data (accessControl.groupClaim, default 'groups'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
143
|
+
},
|
|
144
|
+
"public": {
|
|
145
|
+
"type": "boolean",
|
|
146
|
+
"description": "Access control: mark this capability as available to all callers. In mode 'rolesAndGroups' every capability must set roles/groups OR public: true (not both) — an unclassified capability is a configuration error, so nothing is ever exposed by omission."
|
|
103
147
|
}
|
|
104
148
|
}
|
|
105
149
|
}
|
|
@@ -143,6 +187,28 @@
|
|
|
143
187
|
"type": "object",
|
|
144
188
|
"additionalProperties": true,
|
|
145
189
|
"description": "Arbitrary MCP \\_meta fields to expose downstream."
|
|
190
|
+
},
|
|
191
|
+
"roles": {
|
|
192
|
+
"type": "array",
|
|
193
|
+
"minItems": 1,
|
|
194
|
+
"items": {
|
|
195
|
+
"type": "string",
|
|
196
|
+
"minLength": 1
|
|
197
|
+
},
|
|
198
|
+
"description": "Access control: roles allowed to see and invoke this capability. Matched against the caller's roles on request.user.data (accessControl.roleClaim, default 'roles'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
199
|
+
},
|
|
200
|
+
"groups": {
|
|
201
|
+
"type": "array",
|
|
202
|
+
"minItems": 1,
|
|
203
|
+
"items": {
|
|
204
|
+
"type": "string",
|
|
205
|
+
"minLength": 1
|
|
206
|
+
},
|
|
207
|
+
"description": "Access control: groups allowed to see and invoke this capability. Matched against the caller's groups on request.user.data (accessControl.groupClaim, default 'groups'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
208
|
+
},
|
|
209
|
+
"public": {
|
|
210
|
+
"type": "boolean",
|
|
211
|
+
"description": "Access control: mark this capability as available to all callers. In mode 'rolesAndGroups' every capability must set roles/groups OR public: true (not both) — an unclassified capability is a configuration error, so nothing is ever exposed by omission."
|
|
146
212
|
}
|
|
147
213
|
}
|
|
148
214
|
}
|
|
@@ -186,11 +252,76 @@
|
|
|
186
252
|
"type": "object",
|
|
187
253
|
"additionalProperties": true,
|
|
188
254
|
"description": "Arbitrary MCP \\_meta fields to expose downstream."
|
|
255
|
+
},
|
|
256
|
+
"roles": {
|
|
257
|
+
"type": "array",
|
|
258
|
+
"minItems": 1,
|
|
259
|
+
"items": {
|
|
260
|
+
"type": "string",
|
|
261
|
+
"minLength": 1
|
|
262
|
+
},
|
|
263
|
+
"description": "Access control: roles allowed to see and invoke this capability. Matched against the caller's roles on request.user.data (accessControl.roleClaim, default 'roles'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
264
|
+
},
|
|
265
|
+
"groups": {
|
|
266
|
+
"type": "array",
|
|
267
|
+
"minItems": 1,
|
|
268
|
+
"items": {
|
|
269
|
+
"type": "string",
|
|
270
|
+
"minLength": 1
|
|
271
|
+
},
|
|
272
|
+
"description": "Access control: groups allowed to see and invoke this capability. Matched against the caller's groups on request.user.data (accessControl.groupClaim, default 'groups'). Used when accessControl.mode is 'rolesAndGroups'."
|
|
273
|
+
},
|
|
274
|
+
"public": {
|
|
275
|
+
"type": "boolean",
|
|
276
|
+
"description": "Access control: mark this capability as available to all callers. In mode 'rolesAndGroups' every capability must set roles/groups OR public: true (not both) — an unclassified capability is a configuration error, so nothing is ever exposed by omission."
|
|
189
277
|
}
|
|
190
278
|
}
|
|
191
279
|
}
|
|
192
280
|
]
|
|
193
281
|
}
|
|
282
|
+
},
|
|
283
|
+
"accessControl": {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"additionalProperties": false,
|
|
286
|
+
"description": "Optional per-caller access control. Defaults to mode 'allPublic' (no access control). Set mode to 'rolesAndGroups' to match the caller's role/group claims against each capability's markup (every capability must then set roles/groups or public: true), or 'function' to delegate to a custom resolver.",
|
|
287
|
+
"properties": {
|
|
288
|
+
"mode": {
|
|
289
|
+
"type": "string",
|
|
290
|
+
"enum": ["allPublic", "rolesAndGroups", "function"],
|
|
291
|
+
"default": "allPublic",
|
|
292
|
+
"description": "Access-control strategy. 'allPublic' (default): no access control; roles/groups/public markup is not allowed. 'rolesAndGroups': match the caller's role and group claims against each capability's markup. 'function': delegate to the resolver named by 'identifier'."
|
|
293
|
+
},
|
|
294
|
+
"roleClaim": {
|
|
295
|
+
"type": "string",
|
|
296
|
+
"minLength": 1,
|
|
297
|
+
"default": "roles",
|
|
298
|
+
"description": "Property on request.user.data holding the caller's roles (commonly a JWT 'roles' claim, but request.user.data is set by whichever auth policy ran — JWT, API key, etc.), matched against each capability's 'roles'. Supports a dot-path for nested values, e.g. 'realm\\_access.roles'. Used in mode 'rolesAndGroups'. Defaults to 'roles'."
|
|
299
|
+
},
|
|
300
|
+
"groupClaim": {
|
|
301
|
+
"type": "string",
|
|
302
|
+
"minLength": 1,
|
|
303
|
+
"default": "groups",
|
|
304
|
+
"description": "Property on request.user.data holding the caller's groups (commonly a JWT 'groups' claim, but request.user.data is set by whichever auth policy ran — JWT, API key, etc.), matched against each capability's 'groups'. Supports a dot-path for nested values, e.g. 'cognito:groups'. Used in mode 'rolesAndGroups'. Defaults to 'groups'."
|
|
305
|
+
},
|
|
306
|
+
"identifier": {
|
|
307
|
+
"type": "object",
|
|
308
|
+
"additionalProperties": false,
|
|
309
|
+
"required": ["module", "export"],
|
|
310
|
+
"description": "Custom capability resolver, required when mode is 'function'. It fully replaces built-in roles/groups matching.",
|
|
311
|
+
"properties": {
|
|
312
|
+
"module": {
|
|
313
|
+
"type": "string",
|
|
314
|
+
"minLength": 1,
|
|
315
|
+
"description": "Module to load the resolver from, e.g. $import(./modules/mcp-access-control)."
|
|
316
|
+
},
|
|
317
|
+
"export": {
|
|
318
|
+
"type": "string",
|
|
319
|
+
"minLength": 1,
|
|
320
|
+
"description": "Named export of the resolver function, e.g. 'default'."
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
194
325
|
}
|
|
195
326
|
}
|
|
196
327
|
}
|
|
@@ -200,6 +331,12 @@
|
|
|
200
331
|
"export": "McpCapabilityFilterInboundPolicy",
|
|
201
332
|
"module": "$import(@zuplo/runtime)",
|
|
202
333
|
"options": {
|
|
334
|
+
"accessControl": {
|
|
335
|
+
"groupClaim": "groups",
|
|
336
|
+
"identifier": {},
|
|
337
|
+
"mode": "allPublic",
|
|
338
|
+
"roleClaim": "roles"
|
|
339
|
+
},
|
|
203
340
|
"prompts": [],
|
|
204
341
|
"resourceTemplates": [],
|
|
205
342
|
"resources": [],
|
|
@@ -65,9 +65,47 @@ can discover:
|
|
|
65
65
|
2. The upstream protected resource metadata `scopes_supported` value.
|
|
66
66
|
3. No `scope` parameter if the upstream does not advertise one.
|
|
67
67
|
|
|
68
|
-
Explicit configured scopes always win
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
Explicit configured scopes always win: they are sent as the `scope` parameter on
|
|
69
|
+
the upstream authorization request even when the upstream advertises a different
|
|
70
|
+
`scopes_supported` in its protected resource metadata. Use them for providers
|
|
71
|
+
such as Microsoft 365 where the correct resource-specific application scope is
|
|
72
|
+
known from the provider configuration rather than from MCP discovery, or to add
|
|
73
|
+
`offline_access` when the upstream requires it for refresh-token issuance but
|
|
74
|
+
does not advertise it.
|
|
75
|
+
|
|
76
|
+
## Consent Prompt Override
|
|
77
|
+
|
|
78
|
+
The MCP SDK that the gateway uses as its upstream OAuth client adds
|
|
79
|
+
`prompt=consent` to the authorization request whenever the requested scope
|
|
80
|
+
includes `offline_access`, because most providers require a fresh consent grant
|
|
81
|
+
before issuing a refresh token. The gateway itself never injects `prompt`; the
|
|
82
|
+
`prompt` option strips or replaces the SDK's parameter at the gateway's
|
|
83
|
+
authorization seam. Some providers honor `prompt=consent` literally: Microsoft
|
|
84
|
+
Entra ID forces the consent screen on every single authorization, even after a
|
|
85
|
+
tenant admin has granted org-wide admin consent.
|
|
86
|
+
|
|
87
|
+
Set `prompt` to control this behavior:
|
|
88
|
+
|
|
89
|
+
- **Unset** (default): keep the SDK's behavior, including the automatic
|
|
90
|
+
`prompt=consent` for `offline_access`.
|
|
91
|
+
- **`false`**: strip the `prompt` parameter from the authorization request. Use
|
|
92
|
+
this for Entra upstreams where a tenant admin has already granted admin
|
|
93
|
+
consent — users connect without seeing the consent screen, and refresh tokens
|
|
94
|
+
are still issued because admin consent satisfies the requirement.
|
|
95
|
+
- **`"none"` / `"login"` / `"consent"` / `"select_account"`**: set or replace
|
|
96
|
+
the `prompt` parameter with an explicit value.
|
|
97
|
+
|
|
98
|
+
Note that the effective scope can include `offline_access` even when `scopes` is
|
|
99
|
+
not configured, because the gateway also reads scopes from the upstream's
|
|
100
|
+
`WWW-Authenticate` challenge and protected resource metadata.
|
|
101
|
+
|
|
102
|
+
**Warning**: stripping `prompt=consent` while requesting `offline_access` can
|
|
103
|
+
silently prevent refresh-token issuance on providers that require a fresh
|
|
104
|
+
consent grant for offline access (for example, Google). Only use `false` when
|
|
105
|
+
the provider grants offline access through pre-authorized or admin consent, such
|
|
106
|
+
as Entra tenant admin consent. If connections start requiring frequent
|
|
107
|
+
reauthorization after setting `prompt: false`, remove the option so the consent
|
|
108
|
+
grant is requested again.
|
|
71
109
|
|
|
72
110
|
## Route Shape
|
|
73
111
|
|
|
@@ -71,6 +71,19 @@
|
|
|
71
71
|
"type": "string",
|
|
72
72
|
"description": "Delimiter used to join scopes in the OAuth authorization request. Defaults to a single space."
|
|
73
73
|
},
|
|
74
|
+
"prompt": {
|
|
75
|
+
"description": "Overrides the OIDC `prompt` parameter on upstream authorization requests. The MCP SDK adds `prompt=consent` whenever the requested scope includes `offline_access`, which forces a consent screen on every authorization on providers like Microsoft Entra ID even after a tenant admin has granted org-wide admin consent. Leave unset to keep the SDK's behavior; set to `false` to strip the `prompt` parameter; set an explicit value to replace it. Warning: stripping `prompt=consent` while requesting `offline_access` can silently prevent refresh-token issuance on providers that require a fresh consent grant for offline access (e.g. Google) — only use this when the provider grants offline access via pre-authorized/admin consent (e.g. Entra tenant admin consent).",
|
|
76
|
+
"oneOf": [
|
|
77
|
+
{
|
|
78
|
+
"type": "string",
|
|
79
|
+
"enum": ["none", "login", "consent", "select_account"]
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"type": "boolean",
|
|
83
|
+
"const": false
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
74
87
|
"clientRegistration": {
|
|
75
88
|
"description": "OAuth client registration mode. Defaults to `auto`, which uses Client ID Metadata Documents when the upstream advertises support and falls back to Dynamic Client Registration otherwise.",
|
|
76
89
|
"oneOf": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.72.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The programmable API Gateway",
|
|
6
6
|
"author": "Zuplo, Inc.",
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"zuplo": "zuplo.js"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@zuplo/cli": "6.
|
|
23
|
-
"@zuplo/core": "6.
|
|
24
|
-
"@zuplo/runtime": "6.
|
|
22
|
+
"@zuplo/cli": "6.72.1",
|
|
23
|
+
"@zuplo/core": "6.72.1",
|
|
24
|
+
"@zuplo/runtime": "6.72.1",
|
|
25
25
|
"@zuplo/test": "1.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|