zuplo 6.71.11 → 6.71.13
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/articles/monetization/monetization-policy.md +42 -23
- package/docs/articles/monetization/programmatic-monetization.md +29 -0
- package/docs/dev-portal/zudoku/configuration/navigation.mdx +32 -4
- package/docs/mcp-gateway/cross-app-access/overview.mdx +204 -0
- package/docs/mcp-gateway/cross-app-access/policy-reference.mdx +161 -0
- package/docs/mcp-gateway/cross-app-access/quickstart.mdx +330 -0
- package/package.json +4 -4
|
@@ -61,13 +61,14 @@ reads the API key from the `Authorization` header, validates it, and sets
|
|
|
61
61
|
|
|
62
62
|
## Configuration options
|
|
63
63
|
|
|
64
|
-
| Option
|
|
65
|
-
|
|
|
66
|
-
| `meters`
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `
|
|
64
|
+
| Option | Type | Default | Description |
|
|
65
|
+
| ---------------------- | ------------------ | ----------------- | ----------------------------------------------------- |
|
|
66
|
+
| `meters` | object | _(none)_ | Map of meter keys to increment values |
|
|
67
|
+
| `requiredEntitlements` | string[] | _(none)_ | Entitlement keys the subscription must have access to |
|
|
68
|
+
| `meterOnStatusCodes` | string or number[] | `"200-299"` | Status code range to meter |
|
|
69
|
+
| `authHeader` | string | `"authorization"` | Header to read the API key from |
|
|
70
|
+
| `authScheme` | string | `"Bearer"` | Expected auth scheme prefix |
|
|
71
|
+
| `cacheTtlSeconds` | number | `60` | How long to cache subscription data (minimum 60s) |
|
|
71
72
|
|
|
72
73
|
### `meters`
|
|
73
74
|
|
|
@@ -91,6 +92,23 @@ see the [Dynamic Metering](./dynamic-metering.md) guide.
|
|
|
91
92
|
{ "meters": { "api_credits": 10 } }
|
|
92
93
|
```
|
|
93
94
|
|
|
95
|
+
### `requiredEntitlements`
|
|
96
|
+
|
|
97
|
+
A list of [entitlement](./features.mdx) keys the caller's subscription must have
|
|
98
|
+
access to before the request is allowed. Use it to gate a route on a feature —
|
|
99
|
+
for example, restrict it to plans that include `custom_domains` — without
|
|
100
|
+
writing code.
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{ "requiredEntitlements": ["custom_domains"] }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The request is allowed only when **every** listed entitlement is present with
|
|
107
|
+
`hasAccess` set to `true`. If any is missing, disabled, or has exhausted its
|
|
108
|
+
quota, the policy returns `403 Forbidden` before the request reaches your
|
|
109
|
+
backend. For access checks that need runtime logic, gate in code instead — see
|
|
110
|
+
[Programmatic Monetization](./programmatic-monetization.md).
|
|
111
|
+
|
|
94
112
|
### `meterOnStatusCodes`
|
|
95
113
|
|
|
96
114
|
Controls which responses count toward metering. By default, only successful
|
|
@@ -256,22 +274,23 @@ the RFC 7807 Problem Details format:
|
|
|
256
274
|
|
|
257
275
|
Common error details:
|
|
258
276
|
|
|
259
|
-
| Condition | `detail` message
|
|
260
|
-
| -------------------------------- |
|
|
261
|
-
| No auth header | `"No Authorization Header"`
|
|
262
|
-
| Wrong auth scheme | `"Invalid Authorization Scheme"`
|
|
263
|
-
| Empty key after the auth scheme | `"No key present"`
|
|
264
|
-
| Cached invalid key or 401 | `"Authorization Failed"`
|
|
265
|
-
| Invalid API key | `"API Key is invalid or does not have access to the API"`
|
|
266
|
-
| Expired API key | `"API Key has expired."`
|
|
267
|
-
| Expired subscription | `"API Key has an expired subscription."`
|
|
268
|
-
| Subscription has no payment | `"Subscription payment status is not available."`
|
|
269
|
-
| Payment not made | `"Payment has not been made."`
|
|
270
|
-
| Payment overdue | `"Payment is overdue. Please update your payment method."`
|
|
271
|
-
| Subscription has no entitlements | `"Subscription entitlements are not available."`
|
|
272
|
-
| Meter not in subscription | `"API Key does not have \"X\" meter provided by the subscription."`
|
|
273
|
-
| Quota exhausted | `"API Key has exceeded the allowed limit for \"X\" meter."`
|
|
274
|
-
| Meter access denied | `"API Key does not have access to \"X\" meter."`
|
|
277
|
+
| Condition | `detail` message |
|
|
278
|
+
| -------------------------------- | ---------------------------------------------------------------------------------- |
|
|
279
|
+
| No auth header | `"No Authorization Header"` |
|
|
280
|
+
| Wrong auth scheme | `"Invalid Authorization Scheme"` |
|
|
281
|
+
| Empty key after the auth scheme | `"No key present"` |
|
|
282
|
+
| Cached invalid key or 401 | `"Authorization Failed"` |
|
|
283
|
+
| Invalid API key | `"API Key is invalid or does not have access to the API"` |
|
|
284
|
+
| Expired API key | `"API Key has expired."` |
|
|
285
|
+
| Expired subscription | `"API Key has an expired subscription."` |
|
|
286
|
+
| Subscription has no payment | `"Subscription payment status is not available."` |
|
|
287
|
+
| Payment not made | `"Payment has not been made."` |
|
|
288
|
+
| Payment overdue | `"Payment is overdue. Please update your payment method."` |
|
|
289
|
+
| Subscription has no entitlements | `"Subscription entitlements are not available."` |
|
|
290
|
+
| Meter not in subscription | `"API Key does not have \"X\" meter provided by the subscription."` |
|
|
291
|
+
| Quota exhausted | `"API Key has exceeded the allowed limit for \"X\" meter."` |
|
|
292
|
+
| Meter access denied | `"API Key does not have access to \"X\" meter."` |
|
|
293
|
+
| Required entitlement missing | `"The required \"X\" entitlement is not allowed or its quota has been exhausted."` |
|
|
275
294
|
|
|
276
295
|
## Pipeline ordering
|
|
277
296
|
|
|
@@ -74,6 +74,35 @@ if (!advancedSearch?.hasAccess) {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
+
:::tip
|
|
78
|
+
|
|
79
|
+
If you only need to check that a feature's entitlement has access — with no
|
|
80
|
+
other runtime logic — skip the custom code and use the policy's
|
|
81
|
+
[`requiredEntitlements`](./monetization-policy.md#requiredentitlements) option
|
|
82
|
+
instead. It rejects the request with `403 Forbidden` when any listed entitlement
|
|
83
|
+
is missing or out of quota:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
// config/policies.json
|
|
87
|
+
{
|
|
88
|
+
"name": "monetization-inbound",
|
|
89
|
+
"policyType": "monetization-inbound",
|
|
90
|
+
"handler": {
|
|
91
|
+
"export": "MonetizationInboundPolicy",
|
|
92
|
+
"module": "$import(@zuplo/runtime)",
|
|
93
|
+
"options": {
|
|
94
|
+
"meters": { "api_requests": 1 },
|
|
95
|
+
"requiredEntitlements": ["advanced_search"]
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Reach for the code path below when the decision needs more than a presence check
|
|
102
|
+
— inspecting the plan key, reading the request, or combining conditions.
|
|
103
|
+
|
|
104
|
+
:::
|
|
105
|
+
|
|
77
106
|
Register the policy and apply it after `monetization-inbound` on the routes you
|
|
78
107
|
want to protect:
|
|
79
108
|
|
|
@@ -143,7 +143,10 @@ type NavigationCategory = {
|
|
|
143
143
|
collapsible?: boolean;
|
|
144
144
|
collapsed?: boolean;
|
|
145
145
|
stack?: boolean; // open the category's items as a stacked sub-nav
|
|
146
|
-
link?:
|
|
146
|
+
link?:
|
|
147
|
+
| string
|
|
148
|
+
| { type: "doc"; file: string; label?: string; path?: string }
|
|
149
|
+
| { type: "link"; to: string; label?: string };
|
|
147
150
|
display?:
|
|
148
151
|
| "auth"
|
|
149
152
|
| "anon"
|
|
@@ -158,9 +161,11 @@ type NavigationCategory = {
|
|
|
158
161
|
#### Category links
|
|
159
162
|
|
|
160
163
|
A category can have a `link` property that makes the category label itself clickable, navigating to
|
|
161
|
-
a document. This is useful when you want a category that acts as both a
|
|
164
|
+
a document, a path, or an external URL. This is useful when you want a category that acts as both a
|
|
165
|
+
group and a landing page.
|
|
162
166
|
|
|
163
|
-
The `link` can be a simple string pointing to a file path,
|
|
167
|
+
The `link` can be a simple string pointing to a file path, a `doc` object for more control, or a
|
|
168
|
+
`link` object that points the category label at an arbitrary path or external URL:
|
|
164
169
|
|
|
165
170
|
```tsx title="String shorthand"
|
|
166
171
|
{
|
|
@@ -190,7 +195,7 @@ The `link` can be a simple string pointing to a file path, or an object for more
|
|
|
190
195
|
}
|
|
191
196
|
```
|
|
192
197
|
|
|
193
|
-
The object form supports these properties:
|
|
198
|
+
The `doc` object form supports these properties:
|
|
194
199
|
|
|
195
200
|
| Property | Type | Description |
|
|
196
201
|
| -------- | -------- | -------------------------------------------------------- |
|
|
@@ -199,6 +204,29 @@ The object form supports these properties:
|
|
|
199
204
|
| `label` | `string` | Override the label (defaults to the document title) |
|
|
200
205
|
| `path` | `string` | Custom URL path (overrides the default file-based route) |
|
|
201
206
|
|
|
207
|
+
```tsx title="Link form pointing to a path or external URL"
|
|
208
|
+
{
|
|
209
|
+
type: "category",
|
|
210
|
+
label: "API Reference",
|
|
211
|
+
link: {
|
|
212
|
+
type: "link",
|
|
213
|
+
to: "/api",
|
|
214
|
+
},
|
|
215
|
+
items: [
|
|
216
|
+
"guides/authentication",
|
|
217
|
+
"guides/rate-limits",
|
|
218
|
+
],
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The `link` object form supports these properties:
|
|
223
|
+
|
|
224
|
+
| Property | Type | Description |
|
|
225
|
+
| -------- | -------- | ----------------------------------- |
|
|
226
|
+
| `type` | `"link"` | Must be `"link"` |
|
|
227
|
+
| `to` | `string` | Path or external URL to navigate to |
|
|
228
|
+
| `label` | `string` | Override the category label |
|
|
229
|
+
|
|
202
230
|
### `type: doc`
|
|
203
231
|
|
|
204
232
|
Doc is used to reference markdown files. The `label` is the text that will be displayed, and the
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Cross App Access (XAA)"
|
|
3
|
+
sidebar_label: "Overview"
|
|
4
|
+
description: |
|
|
5
|
+
What Cross App Access (XAA) and the Identity Assertion JWT Authorization Grant
|
|
6
|
+
(ID-JAG) are, the problem they solve for AI agents and MCP, and how the Zuplo
|
|
7
|
+
MCP Gateway performs the token exchange so your MCP servers and clients don't
|
|
8
|
+
have to.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
:::note{title="Beta"}
|
|
12
|
+
|
|
13
|
+
Cross App Access support is in beta and builds on the
|
|
14
|
+
[MCP Gateway](../introduction.mdx), which is also in beta. The configuration
|
|
15
|
+
model and policy options may change before general availability.
|
|
16
|
+
|
|
17
|
+
:::
|
|
18
|
+
|
|
19
|
+
Cross App Access (XAA) lets one application reach another application's API on a
|
|
20
|
+
user's behalf **through the identity provider both apps already trust** —
|
|
21
|
+
instead of running a separate, point-to-point OAuth connection between them. For
|
|
22
|
+
AI agents and MCP, that means an agent can call a protected upstream MCP server
|
|
23
|
+
for a user without the user re-consenting to every app pair, and without
|
|
24
|
+
credentials sprawling outside the identity provider's view.
|
|
25
|
+
|
|
26
|
+
The Zuplo MCP Gateway sits in the middle of this flow and runs the XAA token
|
|
27
|
+
exchange for you. An MCP client connects to the gateway with ordinary MCP OAuth;
|
|
28
|
+
the gateway, acting as the XAA _requesting app_, mints the cross-app grant from
|
|
29
|
+
your identity provider and redeems it at the upstream's authorization server.
|
|
30
|
+
Neither the MCP client nor the upstream MCP server has to implement XAA itself.
|
|
31
|
+
|
|
32
|
+
## The problem XAA solves
|
|
33
|
+
|
|
34
|
+
Single sign-on (SSO) solves _login_: a user authenticates once with an identity
|
|
35
|
+
provider (Okta, Microsoft Entra, Auth0) and gets into every connected app. SSO
|
|
36
|
+
does **not** solve _app-to-app API access_. When one app needs to call another
|
|
37
|
+
app's API on the user's behalf, the two apps today run a direct OAuth flow
|
|
38
|
+
between themselves. The
|
|
39
|
+
[IETF draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/)
|
|
40
|
+
describes this as a connection that "bypasses the trusted identity provider" and
|
|
41
|
+
is "invisible to the identity provider managing the ecosystem."
|
|
42
|
+
|
|
43
|
+
AI agents make this gap urgent. An agent in one app increasingly needs to reach
|
|
44
|
+
into other SaaS apps' data on the user's behalf. The naive approach produces:
|
|
45
|
+
|
|
46
|
+
- **Consent-screen sprawl** — the user re-authorizes every agent against every
|
|
47
|
+
downstream app individually.
|
|
48
|
+
- **Credential sprawl** — long-lived tokens scattered per app pair, invisible to
|
|
49
|
+
IT.
|
|
50
|
+
- **No central governance** — security teams can't see or control what connects
|
|
51
|
+
to what, and offboarding means hunting access down service by service.
|
|
52
|
+
|
|
53
|
+
XAA routes app-to-app access back through the identity provider both apps
|
|
54
|
+
already trust. The IdP becomes the single place where IT decides which app can
|
|
55
|
+
talk to which app, enforces conditional-access policy, and audits every
|
|
56
|
+
delegation — while the upstream app's authorization server stays the
|
|
57
|
+
access-token issuer. The trust boundary isn't broken; it's brokered.
|
|
58
|
+
|
|
59
|
+
## How the flow works
|
|
60
|
+
|
|
61
|
+
XAA is defined by the **Identity Assertion JWT Authorization Grant (ID-JAG)** —
|
|
62
|
+
`draft-ietf-oauth-identity-assertion-authz-grant`. It chains two OAuth exchanges
|
|
63
|
+
across two authorization servers:
|
|
64
|
+
|
|
65
|
+
1. **Identity assertion → ID-JAG**, at the identity provider, using
|
|
66
|
+
[RFC 8693 token exchange](https://www.rfc-editor.org/rfc/rfc8693). The
|
|
67
|
+
requesting app presents the user's ID token (the identity assertion) and asks
|
|
68
|
+
for a grant scoped to one specific target authorization server. The IdP
|
|
69
|
+
evaluates policy and returns a short-lived, audience-restricted **ID-JAG** —
|
|
70
|
+
a signed JWT with the header `typ: oauth-id-jag+jwt`.
|
|
71
|
+
2. **ID-JAG → access token**, at the upstream's resource authorization server,
|
|
72
|
+
using the
|
|
73
|
+
[RFC 7523 JWT bearer grant](https://www.rfc-editor.org/rfc/rfc7523). The
|
|
74
|
+
requesting app presents the ID-JAG as an `assertion` and receives a normal
|
|
75
|
+
access token for the upstream API.
|
|
76
|
+
|
|
77
|
+
The ID-JAG is a _grant_, not an access token — it can't call an API directly. It
|
|
78
|
+
exists only to be redeemed in the second exchange.
|
|
79
|
+
|
|
80
|
+
When the Zuplo gateway fronts an XAA-protected upstream, the gateway plays the
|
|
81
|
+
**requesting app** for both exchanges:
|
|
82
|
+
|
|
83
|
+
<Diagram height="h-80">
|
|
84
|
+
<DiagramNode id="client">MCP Client</DiagramNode>
|
|
85
|
+
<DiagramGroup id="gateway" label="Zuplo MCP Gateway (requesting app)">
|
|
86
|
+
<DiagramNode id="inbound" variant="zuplo">
|
|
87
|
+
OAuth 2.1 server
|
|
88
|
+
</DiagramNode>
|
|
89
|
+
<DiagramNode id="requestor" variant="zuplo">
|
|
90
|
+
XAA requestor
|
|
91
|
+
</DiagramNode>
|
|
92
|
+
</DiagramGroup>
|
|
93
|
+
<DiagramNode id="idp">Identity Provider</DiagramNode>
|
|
94
|
+
<DiagramGroup id="upstream" label="Upstream app">
|
|
95
|
+
<DiagramNode id="ras">Resource AS</DiagramNode>
|
|
96
|
+
<DiagramNode id="mcp">MCP server</DiagramNode>
|
|
97
|
+
</DiagramGroup>
|
|
98
|
+
<DiagramEdge from="client" to="inbound" label="MCP OAuth (no XAA)" />
|
|
99
|
+
<DiagramEdge from="requestor" to="idp" label="1. ID token → ID-JAG" />
|
|
100
|
+
<DiagramEdge from="requestor" to="ras" label="2. ID-JAG → access token" />
|
|
101
|
+
<DiagramEdge from="requestor" to="mcp" label="3. Bearer token" />
|
|
102
|
+
</Diagram>
|
|
103
|
+
|
|
104
|
+
Step by step, for a tool call to an XAA-protected upstream route:
|
|
105
|
+
|
|
106
|
+
1. The MCP client connects to the gateway route with **ordinary MCP OAuth** —
|
|
107
|
+
discovery, PKCE, and a bearer token issued by the gateway. This leg is _not_
|
|
108
|
+
XAA. During the gateway's browser-login step, the user authenticates with the
|
|
109
|
+
identity provider, and the gateway captures the user's IdP identity
|
|
110
|
+
assertion.
|
|
111
|
+
2. On a tool call, the gateway exchanges the user's ID token at the IdP for an
|
|
112
|
+
**ID-JAG** audience-restricted to the upstream's resource authorization
|
|
113
|
+
server (RFC 8693 token exchange).
|
|
114
|
+
3. The gateway redeems the ID-JAG at the upstream resource authorization server
|
|
115
|
+
for an upstream **access token** (RFC 7523 JWT bearer grant).
|
|
116
|
+
4. The gateway forwards the tool call to the upstream MCP server with
|
|
117
|
+
`Authorization: Bearer <upstream access token>` and caches the upstream token
|
|
118
|
+
per user for subsequent calls.
|
|
119
|
+
|
|
120
|
+
:::note{title="In requesting-app mode, the client → gateway leg is plain OAuth"}
|
|
121
|
+
|
|
122
|
+
This is the most common point of confusion. In the flow above, the MCP client
|
|
123
|
+
never speaks XAA — it runs the standard MCP authorization flow against the
|
|
124
|
+
gateway, and all ID-JAG exchanges happen on the gateway's _outbound_ side. A
|
|
125
|
+
client doesn't need any XAA support to benefit from it.
|
|
126
|
+
|
|
127
|
+
:::
|
|
128
|
+
|
|
129
|
+
## What the gateway does for you
|
|
130
|
+
|
|
131
|
+
In the XAA flow the gateway plays the requesting app, so the MCP client and the
|
|
132
|
+
upstream MCP server are each insulated from the protocol:
|
|
133
|
+
|
|
134
|
+
- **The MCP client** does nothing new. It runs the same MCP OAuth flow it would
|
|
135
|
+
for any gateway route. Claude, Cursor, ChatGPT, VS Code, and any
|
|
136
|
+
spec-compliant client work unchanged.
|
|
137
|
+
- **The two-exchange dance** — the RFC 8693 token exchange at the IdP and the
|
|
138
|
+
RFC 7523 JWT-bearer redemption at the upstream — runs inside the gateway. You
|
|
139
|
+
configure endpoints and credentials; the gateway mints, redeems, caches, and
|
|
140
|
+
refreshes.
|
|
141
|
+
- **Per-user identity** is preserved end to end. The IdP sees the specific user,
|
|
142
|
+
the upstream sees a token minted for that user, and gateway analytics record
|
|
143
|
+
the same subject.
|
|
144
|
+
|
|
145
|
+
## Glossary
|
|
146
|
+
|
|
147
|
+
- **Identity assertion** — Proof the IdP authenticated the user, carried as an
|
|
148
|
+
OIDC ID token (`urn:ietf:params:oauth:token-type:id_token`) or SAML 2.0
|
|
149
|
+
assertion. It's the _input_ to the XAA flow.
|
|
150
|
+
- **ID-JAG (Identity Assertion JWT Authorization Grant)** — A short-lived signed
|
|
151
|
+
JWT the IdP mints, audience-restricted to one resource authorization server.
|
|
152
|
+
Used once to obtain an access token there. JWT header `typ: oauth-id-jag+jwt`;
|
|
153
|
+
token type `urn:ietf:params:oauth:token-type:id-jag`. It is **not** an access
|
|
154
|
+
token.
|
|
155
|
+
- **Identity provider (IdP)** — The SSO authority both apps trust (Okta, Entra,
|
|
156
|
+
Auth0). In XAA it also brokers cross-app access by issuing ID-JAGs after
|
|
157
|
+
evaluating policy. Identified by `iss` in the ID-JAG.
|
|
158
|
+
- **Resource authorization server** — The _target_ app's OAuth server. It
|
|
159
|
+
consumes the ID-JAG via the JWT-bearer grant and issues the real access token.
|
|
160
|
+
Identified by `aud` in the ID-JAG. In Zuplo's outbound flow, this is the
|
|
161
|
+
upstream's server, not the gateway.
|
|
162
|
+
- **Requesting app** — The party that obtains and redeems the ID-JAG. When the
|
|
163
|
+
gateway fronts an XAA-protected upstream, the gateway is the requesting app.
|
|
164
|
+
- **Token exchange (RFC 8693)** — The grant used at the IdP to swap the identity
|
|
165
|
+
assertion for the ID-JAG.
|
|
166
|
+
- **JWT bearer grant (RFC 7523)** — The grant used at the resource authorization
|
|
167
|
+
server to redeem the ID-JAG for an access token.
|
|
168
|
+
|
|
169
|
+
## When to use XAA
|
|
170
|
+
|
|
171
|
+
Reach for XAA when:
|
|
172
|
+
|
|
173
|
+
- An upstream MCP server is protected by an **enterprise resource authorization
|
|
174
|
+
server** that accepts ID-JAGs (the "enterprise-managed authorization"
|
|
175
|
+
pattern), and you want the gateway to broker access through your IdP rather
|
|
176
|
+
than running a separate OAuth client per upstream.
|
|
177
|
+
- You need **central governance** — one place in the IdP to grant, audit, and
|
|
178
|
+
revoke which apps can reach which APIs on a user's behalf.
|
|
179
|
+
|
|
180
|
+
For an upstream that uses plain per-user or shared OAuth (Linear, Notion,
|
|
181
|
+
Stripe, and most SaaS MCP servers today), use
|
|
182
|
+
[upstream OAuth](../auth/upstream-oauth.mdx) instead — XAA isn't required.
|
|
183
|
+
|
|
184
|
+
## Try it and configure it
|
|
185
|
+
|
|
186
|
+
- [Quickstart](./quickstart.mdx) — see XAA work end to end in minutes using
|
|
187
|
+
Okta's hosted [xaa.dev](https://xaa.dev) playground, with no identity tenant
|
|
188
|
+
required.
|
|
189
|
+
- [Configuration reference](./policy-reference.mdx) — every `idJag` option on
|
|
190
|
+
the token-exchange policy.
|
|
191
|
+
|
|
192
|
+
## Learn more
|
|
193
|
+
|
|
194
|
+
- [Identity Assertion JWT Authorization Grant (IETF draft)](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-assertion-authz-grant/)
|
|
195
|
+
— the ID-JAG specification. Note this is an active draft, not yet a finalized
|
|
196
|
+
RFC.
|
|
197
|
+
- [MCP authorization spec](https://modelcontextprotocol.io/specification/latest/basic/authorization)
|
|
198
|
+
and the
|
|
199
|
+
[Enterprise-Managed Authorization extension](https://modelcontextprotocol.io/extensions/auth/enterprise-managed-authorization)
|
|
200
|
+
— where ID-JAG plugs into MCP.
|
|
201
|
+
- [Okta — Cross App Access](https://developer.okta.com/blog/2025/09/03/cross-app-access)
|
|
202
|
+
— Okta's explainer and walkthrough.
|
|
203
|
+
- [How the MCP Gateway works](../how-it-works.mdx) — the gateway's two OAuth
|
|
204
|
+
surfaces and request lifecycle.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Cross App Access configuration reference"
|
|
3
|
+
sidebar_label: "Configuration reference"
|
|
4
|
+
description: |
|
|
5
|
+
Every configuration option for Cross App Access (XAA) on the Zuplo MCP Gateway
|
|
6
|
+
— the id-jag mode on the token-exchange policy, where the gateway acts as the
|
|
7
|
+
XAA requesting app.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
Cross App Access is configured on the
|
|
11
|
+
[`mcp-token-exchange-inbound`](#gateway-as-requesting-app) policy. Setting
|
|
12
|
+
`authMode: "id-jag"` and providing an `idJag` block makes the gateway act as the
|
|
13
|
+
XAA **requesting app**: it mints an ID-JAG from your IdP and redeems it at an
|
|
14
|
+
upstream resource authorization server. This is the configuration the
|
|
15
|
+
[quickstart](./quickstart.mdx) uses.
|
|
16
|
+
|
|
17
|
+
:::note
|
|
18
|
+
|
|
19
|
+
The authoritative source for these options is the policy's runtime schema. The
|
|
20
|
+
generated `mcp-token-exchange-inbound` reference page predates `id-jag` mode;
|
|
21
|
+
the options below reflect the runtime behavior.
|
|
22
|
+
|
|
23
|
+
:::
|
|
24
|
+
|
|
25
|
+
## Gateway as requesting app
|
|
26
|
+
|
|
27
|
+
Set `authMode: "id-jag"` on a `mcp-token-exchange-inbound` policy and provide an
|
|
28
|
+
`idJag` block. Attach the policy to the upstream route after the inbound MCP
|
|
29
|
+
OAuth policy.
|
|
30
|
+
|
|
31
|
+
```jsonc title="config/policies.json"
|
|
32
|
+
{
|
|
33
|
+
"name": "id-jag-upstream",
|
|
34
|
+
"policyType": "mcp-token-exchange-inbound",
|
|
35
|
+
"handler": {
|
|
36
|
+
"module": "$import(@zuplo/runtime/mcp-gateway)",
|
|
37
|
+
"export": "McpTokenExchangeInboundPolicy",
|
|
38
|
+
"options": {
|
|
39
|
+
"displayName": "Upstream",
|
|
40
|
+
"authMode": "id-jag",
|
|
41
|
+
"idJag": {
|
|
42
|
+
"scopes": ["mcp:tools"],
|
|
43
|
+
"scopeDelimiter": " ",
|
|
44
|
+
"idp": {
|
|
45
|
+
"tokenUrl": "https://idp.example.com/token",
|
|
46
|
+
"clientAuth": {
|
|
47
|
+
"method": "client_secret_post",
|
|
48
|
+
"clientId": "$env(IDP_CLIENT_ID)",
|
|
49
|
+
"clientSecret": "$env(IDP_CLIENT_SECRET)",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
"resourceAs": {
|
|
53
|
+
"tokenUrl": "https://upstream.example.com/token",
|
|
54
|
+
"audience": "https://upstream.example.com",
|
|
55
|
+
"resource": "https://upstream.example.com/mcp",
|
|
56
|
+
"clientAuth": {
|
|
57
|
+
"method": "client_secret_post",
|
|
58
|
+
"clientId": "$env(RESOURCE_AS_CLIENT_ID)",
|
|
59
|
+
"clientSecret": "$env(RESOURCE_AS_CLIENT_SECRET)",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### idJag options
|
|
69
|
+
|
|
70
|
+
| Option | Type | Default | Description |
|
|
71
|
+
| ---------------- | ---------- | ------- | -------------------------------------------------------------------- |
|
|
72
|
+
| `scopes` | `string[]` | `[]` | Scopes requested in both exchanges. |
|
|
73
|
+
| `scopeDelimiter` | `string` | `" "` | Delimiter used to join scopes. |
|
|
74
|
+
| `idp` | object | — | Where the gateway mints the ID-JAG (RFC 8693 token exchange). |
|
|
75
|
+
| `resourceAs` | object | — | Where the gateway redeems the ID-JAG for an access token (RFC 7523). |
|
|
76
|
+
|
|
77
|
+
### idp
|
|
78
|
+
|
|
79
|
+
The identity provider that issues the ID-JAG.
|
|
80
|
+
|
|
81
|
+
| Option | Type | Description |
|
|
82
|
+
| ------------ | ------ | ----------------------------------------------------- |
|
|
83
|
+
| `tokenUrl` | string | The IdP token endpoint. |
|
|
84
|
+
| `clientAuth` | object | How the gateway authenticates to the IdP (see below). |
|
|
85
|
+
|
|
86
|
+
### resourceAs
|
|
87
|
+
|
|
88
|
+
The upstream's resource authorization server that issues the access token.
|
|
89
|
+
|
|
90
|
+
| Option | Type | Description |
|
|
91
|
+
| ------------ | ------ | --------------------------------------------------------------------------------------------------------------------- |
|
|
92
|
+
| `tokenUrl` | string | The resource authorization server's token endpoint. |
|
|
93
|
+
| `audience` | string | **Required.** The resource AS identifier; sent as the token-exchange `audience` and becomes the ID-JAG `aud`. |
|
|
94
|
+
| `resource` | string | Optional [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707) resource indicator. Defaults to the route's upstream URL. |
|
|
95
|
+
| `clientAuth` | object | How the gateway authenticates to the resource AS (see below). |
|
|
96
|
+
|
|
97
|
+
### clientAuth
|
|
98
|
+
|
|
99
|
+
Both `idp.clientAuth` and `resourceAs.clientAuth` take the same shape. The
|
|
100
|
+
`method` selects how the gateway authenticates:
|
|
101
|
+
|
|
102
|
+
```jsonc
|
|
103
|
+
// client_secret_post (or client_secret_basic)
|
|
104
|
+
"clientAuth": {
|
|
105
|
+
"method": "client_secret_post",
|
|
106
|
+
"clientId": "...",
|
|
107
|
+
"clientSecret": "$env(...)"
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```jsonc
|
|
112
|
+
// private_key_jwt
|
|
113
|
+
"clientAuth": {
|
|
114
|
+
"method": "private_key_jwt",
|
|
115
|
+
"clientId": "...",
|
|
116
|
+
"privateKeyPem": "$env(...)",
|
|
117
|
+
"algorithm": "RS256",
|
|
118
|
+
"keyId": "...",
|
|
119
|
+
"expiresInSeconds": 300
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Option | Type | Default | Applies to | Description |
|
|
124
|
+
| ------------------ | ------ | ------- | ----------------- | ------------------------------------------------------------------ |
|
|
125
|
+
| `method` | enum | — | all | `client_secret_post`, `client_secret_basic`, or `private_key_jwt`. |
|
|
126
|
+
| `clientId` | string | — | all | The OAuth client ID. |
|
|
127
|
+
| `clientSecret` | string | — | secret methods | The OAuth client secret. |
|
|
128
|
+
| `privateKeyPem` | string | — | `private_key_jwt` | PEM private key used to sign the client-assertion JWT. |
|
|
129
|
+
| `algorithm` | enum | `RS256` | `private_key_jwt` | `RS256`/`RS384`/`RS512`/`ES256`/`ES384`/`ES512`. |
|
|
130
|
+
| `keyId` | string | — | `private_key_jwt` | Optional `kid` header on the client assertion. |
|
|
131
|
+
| `audience` | string | — | `private_key_jwt` | Optional audience override for the client assertion. |
|
|
132
|
+
| `expiresInSeconds` | number | `300` | `private_key_jwt` | Client-assertion lifetime, max `3600`. |
|
|
133
|
+
|
|
134
|
+
### What the gateway does at request time
|
|
135
|
+
|
|
136
|
+
On a tool call to the route, the gateway:
|
|
137
|
+
|
|
138
|
+
1. Resolves the user's stored IdP identity assertion (bound during the inbound
|
|
139
|
+
browser login). If absent or expired, it returns a connect-required error and
|
|
140
|
+
refreshes the subject token when it can.
|
|
141
|
+
2. Runs an RFC 8693 token exchange at `idp.tokenUrl`, requesting an ID-JAG
|
|
142
|
+
audience-restricted to `resourceAs.audience`.
|
|
143
|
+
3. Redeems the ID-JAG at `resourceAs.tokenUrl` as an RFC 7523 JWT-bearer grant
|
|
144
|
+
to get the upstream access token.
|
|
145
|
+
4. Caches the upstream token per user and forwards the tool call with
|
|
146
|
+
`Authorization: Bearer <upstream token>`.
|
|
147
|
+
|
|
148
|
+
## Notes and limitations
|
|
149
|
+
|
|
150
|
+
- The gateway issues **opaque** access tokens, not JWTs.
|
|
151
|
+
- **DPoP is not supported.** Requests with a `DPoP` header are rejected.
|
|
152
|
+
- XAA requires a prior inbound browser login so the gateway has an IdP identity
|
|
153
|
+
assertion to exchange.
|
|
154
|
+
|
|
155
|
+
## Related
|
|
156
|
+
|
|
157
|
+
- [Overview](./overview.mdx) — the protocol and the gateway's role.
|
|
158
|
+
- [Quickstart](./quickstart.mdx) — a working `id-jag` configuration on the
|
|
159
|
+
playground.
|
|
160
|
+
- [`mcp-token-exchange-inbound` policy](/policies/mcp-token-exchange-inbound) —
|
|
161
|
+
the base token-exchange policy reference.
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Cross App Access quickstart"
|
|
3
|
+
sidebar_label: "Quickstart"
|
|
4
|
+
description: |
|
|
5
|
+
Build a Zuplo MCP Gateway that reaches an XAA-protected MCP server in the
|
|
6
|
+
Zuplo Portal — no repository to clone. The gateway fronts Okta's hosted
|
|
7
|
+
xaa.dev Todo0 server and performs the Cross App Access (XAA) token exchange on
|
|
8
|
+
every request.
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
This quickstart builds a working Cross App Access (XAA) gateway entirely in the
|
|
12
|
+
Zuplo Portal. An MCP client connects to the gateway with ordinary OAuth; the
|
|
13
|
+
gateway then performs the XAA token exchange against
|
|
14
|
+
[Okta's hosted xaa.dev playground](https://xaa.dev) and reaches the protected
|
|
15
|
+
Todo0 MCP server on the user's behalf.
|
|
16
|
+
|
|
17
|
+
There is no repository to clone and no identity tenant to stand up. Everything
|
|
18
|
+
runs in the portal against the public xaa.dev playground.
|
|
19
|
+
|
|
20
|
+
## Why xaa.dev
|
|
21
|
+
|
|
22
|
+
[xaa.dev](https://xaa.dev) is Okta's hosted Cross App Access playground: a
|
|
23
|
+
public identity provider, resource authorization server, and Todo0 MCP server
|
|
24
|
+
that are already wired together for the XAA token exchange. Standing up your own
|
|
25
|
+
XAA stack means provisioning an IdP, a resource authorization server, and a
|
|
26
|
+
protected MCP server — and configuring the trust relationships between all
|
|
27
|
+
three. The playground gives you all of that for free, so today it's the fastest
|
|
28
|
+
way to see a real Cross App Access flow end to end. Once the gateway works
|
|
29
|
+
against xaa.dev, swapping in your own identity provider and upstream is just a
|
|
30
|
+
change of endpoints and credentials.
|
|
31
|
+
|
|
32
|
+
## What you'll build
|
|
33
|
+
|
|
34
|
+
A single gateway route with two policies does all the work:
|
|
35
|
+
|
|
36
|
+
- An **MCP OAuth** policy (`mcp-oauth-inbound`) secures the client → gateway
|
|
37
|
+
leg. The client authenticates to the gateway with plain MCP OAuth — this leg
|
|
38
|
+
is not XAA.
|
|
39
|
+
- A **token-exchange** policy (`mcp-token-exchange-inbound`) in `id-jag` mode
|
|
40
|
+
performs the outbound XAA exchange to the upstream: it mints an ID-JAG at the
|
|
41
|
+
playground identity provider (IdenX), redeems it at the resource authorization
|
|
42
|
+
server, and calls the Todo0 MCP server with the resulting access token.
|
|
43
|
+
|
|
44
|
+
The XAA exchange happens entirely on the gateway's outbound side. See
|
|
45
|
+
[the overview](./overview.mdx#how-the-flow-works) for the full sequence.
|
|
46
|
+
|
|
47
|
+
## Prerequisites
|
|
48
|
+
|
|
49
|
+
- A [Zuplo account](https://portal.zuplo.com)
|
|
50
|
+
- A free account on [xaa.dev](https://xaa.dev)
|
|
51
|
+
- [MCP Jam](https://www.mcpjam.com) — a browser-based MCP client used to drive
|
|
52
|
+
the flow. Any MCP client that supports OAuth works.
|
|
53
|
+
|
|
54
|
+
## Steps
|
|
55
|
+
|
|
56
|
+
<Stepper>
|
|
57
|
+
|
|
58
|
+
1. **Create a Zuplo project.**
|
|
59
|
+
|
|
60
|
+
In the [Zuplo Portal](https://portal.zuplo.com/+/account/projects), select
|
|
61
|
+
**New Project** and create an empty **API Gateway** project. Name it
|
|
62
|
+
`xaa-quick-start`.
|
|
63
|
+
|
|
64
|
+
On the project **Overview** page, copy the deployment URL shown at the top
|
|
65
|
+
(for example, `https://xaa-quick-start-main-abc1234.d2.zuplo.dev`). This is
|
|
66
|
+
your gateway's public origin — you'll need it for the next two steps.
|
|
67
|
+
|
|
68
|
+
:::note
|
|
69
|
+
|
|
70
|
+
Throughout this guide, replace `https://your-gateway.zuplo.dev` with your
|
|
71
|
+
project's actual deployment URL.
|
|
72
|
+
|
|
73
|
+
:::
|
|
74
|
+
|
|
75
|
+
2. **Register a requesting app on xaa.dev.**
|
|
76
|
+
|
|
77
|
+
Sign in to [xaa.dev](https://xaa.dev) and open
|
|
78
|
+
[the developer registration page](https://xaa.dev/developer/register). Enter
|
|
79
|
+
your email, then select **Register New App** and fill in:
|
|
80
|
+
- **Application Name** — anything, for example `xaa-quick-start gateway`.
|
|
81
|
+
- **Redirect URIs** — your gateway's OAuth callback:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
https://your-gateway.zuplo.dev/__zuplo/oauth/callback
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Under **Resource Connections**, select **Todo0 MCP Server**, keep both scopes
|
|
88
|
+
(`todos.read` and `mcp.access`) checked, and select **Add Connection**. Then
|
|
89
|
+
select **Register App**.
|
|
90
|
+
|
|
91
|
+
<BrowserScreenshot url="https://xaa.dev/developer/register">
|
|
92
|
+
|
|
93
|
+

|
|
94
|
+
|
|
95
|
+
</BrowserScreenshot>
|
|
96
|
+
|
|
97
|
+
Registration shows four values. Copy all of them — the secrets are shown only
|
|
98
|
+
once:
|
|
99
|
+
|
|
100
|
+
| xaa.dev value | Used for |
|
|
101
|
+
| ---------------------- | ------------------------------- |
|
|
102
|
+
| Client ID | `XAA_CLIENT_ID` |
|
|
103
|
+
| Client Secret | `XAA_CLIENT_SECRET` |
|
|
104
|
+
| Resource Client ID | `XAA_RESOURCE_AS_CLIENT_ID` |
|
|
105
|
+
| Resource Client Secret | `XAA_RESOURCE_AS_CLIENT_SECRET` |
|
|
106
|
+
|
|
107
|
+
:::tip
|
|
108
|
+
|
|
109
|
+
The playground identity provider (IdenX) accepts any email with no password,
|
|
110
|
+
so you can sign in as any test user when you drive the flow.
|
|
111
|
+
|
|
112
|
+
:::
|
|
113
|
+
|
|
114
|
+
3. **Add the gateway configuration.**
|
|
115
|
+
|
|
116
|
+
Open the project's code editor (the **Code** tab). The gateway needs three
|
|
117
|
+
files.
|
|
118
|
+
|
|
119
|
+
First, define the two policies. Open `config/policies.json` (use the raw
|
|
120
|
+
`policies.json` tab, not the visual Policy List) and replace its contents:
|
|
121
|
+
|
|
122
|
+
```json title="config/policies.json"
|
|
123
|
+
{
|
|
124
|
+
"policies": [
|
|
125
|
+
{
|
|
126
|
+
"name": "xaa-inbound",
|
|
127
|
+
"policyType": "mcp-oauth-inbound",
|
|
128
|
+
"handler": {
|
|
129
|
+
"module": "$import(@zuplo/runtime/mcp-gateway)",
|
|
130
|
+
"export": "McpOAuthInboundPolicy",
|
|
131
|
+
"options": {
|
|
132
|
+
"oidc": {
|
|
133
|
+
"issuer": "https://idp.xaa.dev",
|
|
134
|
+
"jwksUrl": "https://idp.xaa.dev/jwks",
|
|
135
|
+
"audience": "$env(GATEWAY_AUDIENCE)"
|
|
136
|
+
},
|
|
137
|
+
"browserLogin": {
|
|
138
|
+
"url": "https://idp.xaa.dev/authorize",
|
|
139
|
+
"tokenUrl": "https://idp.xaa.dev/token",
|
|
140
|
+
"clientId": "$env(XAA_CLIENT_ID)",
|
|
141
|
+
"clientSecret": "$env(XAA_CLIENT_SECRET)",
|
|
142
|
+
"scope": "openid profile email",
|
|
143
|
+
"audience": "$env(GATEWAY_AUDIENCE)",
|
|
144
|
+
"pkce": "S256"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
"name": "id-jag-upstream",
|
|
151
|
+
"policyType": "mcp-token-exchange-inbound",
|
|
152
|
+
"handler": {
|
|
153
|
+
"module": "$import(@zuplo/runtime/mcp-gateway)",
|
|
154
|
+
"export": "McpTokenExchangeInboundPolicy",
|
|
155
|
+
"options": {
|
|
156
|
+
"id": "id-jag-upstream",
|
|
157
|
+
"displayName": "Todo0",
|
|
158
|
+
"summary": "xaa.dev Todo0 MCP server, reached via Cross App Access (ID-JAG).",
|
|
159
|
+
"authMode": "id-jag",
|
|
160
|
+
"idJag": {
|
|
161
|
+
"scopes": ["todos.read", "mcp.access"],
|
|
162
|
+
"idp": {
|
|
163
|
+
"tokenUrl": "https://idp.xaa.dev/token",
|
|
164
|
+
"clientAuth": {
|
|
165
|
+
"method": "client_secret_post",
|
|
166
|
+
"clientId": "$env(XAA_CLIENT_ID)",
|
|
167
|
+
"clientSecret": "$env(XAA_CLIENT_SECRET)"
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
"resourceAs": {
|
|
171
|
+
"tokenUrl": "https://auth.resource.xaa.dev/token",
|
|
172
|
+
"audience": "https://auth.resource.xaa.dev",
|
|
173
|
+
"resource": "https://mcp.xaa.dev/mcp",
|
|
174
|
+
"clientAuth": {
|
|
175
|
+
"method": "client_secret_post",
|
|
176
|
+
"clientId": "$env(XAA_RESOURCE_AS_CLIENT_ID)",
|
|
177
|
+
"clientSecret": "$env(XAA_RESOURCE_AS_CLIENT_SECRET)"
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
The IdP, resource authorization server, and upstream endpoints are wired to
|
|
189
|
+
the playground here. Only the credentials and the gateway audience are
|
|
190
|
+
environment-driven, so you can plug in your own xaa.dev app. For every
|
|
191
|
+
option, see the [configuration reference](./policy-reference.mdx).
|
|
192
|
+
|
|
193
|
+
Next, add the route that exposes the gateway. Open `config/routes.oas.json`
|
|
194
|
+
(the raw `routes.oas.json` tab) and replace its contents:
|
|
195
|
+
|
|
196
|
+
```json title="config/routes.oas.json"
|
|
197
|
+
{
|
|
198
|
+
"openapi": "3.1.0",
|
|
199
|
+
"info": {
|
|
200
|
+
"version": "1.0.0",
|
|
201
|
+
"title": "XAA MCP Gateway",
|
|
202
|
+
"description": "MCP gateway that bridges to the xaa.dev Todo0 MCP server via Cross App Access (ID-JAG)."
|
|
203
|
+
},
|
|
204
|
+
"paths": {
|
|
205
|
+
"/mcp/todo0": {
|
|
206
|
+
"post": {
|
|
207
|
+
"operationId": "todo0Bridge",
|
|
208
|
+
"summary": "Bridge to the xaa.dev Todo0 MCP server",
|
|
209
|
+
"x-zuplo-route": {
|
|
210
|
+
"corsPolicy": "none",
|
|
211
|
+
"handler": {
|
|
212
|
+
"module": "$import(@zuplo/runtime/mcp-gateway)",
|
|
213
|
+
"export": "McpProxyHandler",
|
|
214
|
+
"options": {
|
|
215
|
+
"rewritePattern": "https://mcp.xaa.dev/mcp"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
"policies": {
|
|
219
|
+
"inbound": ["xaa-inbound", "id-jag-upstream"]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The `McpProxyHandler` forwards the request to the upstream Todo0 server, and
|
|
229
|
+
both policies run inbound — first the MCP OAuth check, then the XAA exchange.
|
|
230
|
+
|
|
231
|
+
Finally, register the MCP Gateway plugin. In the file tree, right-click the
|
|
232
|
+
`modules` folder, select **New Runtime Extension**, and replace the generated
|
|
233
|
+
file's contents:
|
|
234
|
+
|
|
235
|
+
```typescript title="modules/zuplo.runtime.ts"
|
|
236
|
+
import { RuntimeExtensions } from "@zuplo/runtime";
|
|
237
|
+
import { McpGatewayPlugin } from "@zuplo/runtime/mcp-gateway";
|
|
238
|
+
|
|
239
|
+
// Registers the MCP Gateway, which adds the OAuth and upstream-connection
|
|
240
|
+
// routes used to expose and secure MCP servers through your gateway.
|
|
241
|
+
// Docs: https://zuplo.com/docs/mcp-server/introduction
|
|
242
|
+
export function runtimeInit(runtime: RuntimeExtensions) {
|
|
243
|
+
runtime.addPlugin(new McpGatewayPlugin());
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Save each file. The build fails until the environment variables exist —
|
|
248
|
+
that's the next step.
|
|
249
|
+
|
|
250
|
+
4. **Set the environment variables.**
|
|
251
|
+
|
|
252
|
+
Open **Settings →
|
|
253
|
+
[Environment Variables](https://portal.zuplo.com/+/account/project/settings/environment-variables)**
|
|
254
|
+
and add each variable below with **Add variable**. Mark the two `*_SECRET`
|
|
255
|
+
values as **Secret** so they're encrypted and hidden after saving. Leave all
|
|
256
|
+
three environments (Production, Preview, Development) selected.
|
|
257
|
+
|
|
258
|
+
| Variable | Value | Secret |
|
|
259
|
+
| ------------------------------- | ----------------------------------------------------------------------------- | ------ |
|
|
260
|
+
| `GATEWAY_AUDIENCE` | Your gateway's deployment URL (for example, `https://your-gateway.zuplo.dev`) | No |
|
|
261
|
+
| `XAA_CLIENT_ID` | Client ID from the xaa.dev app | No |
|
|
262
|
+
| `XAA_CLIENT_SECRET` | Client Secret from the xaa.dev app | Yes |
|
|
263
|
+
| `XAA_RESOURCE_AS_CLIENT_ID` | Resource Client ID from the Todo0 connection | No |
|
|
264
|
+
| `XAA_RESOURCE_AS_CLIENT_SECRET` | Resource Client Secret from the Todo0 connection | Yes |
|
|
265
|
+
|
|
266
|
+
Saving an environment variable triggers a new deployment. Once it finishes,
|
|
267
|
+
the gateway is live.
|
|
268
|
+
|
|
269
|
+
5. **Connect with MCP Jam and list the todos.**
|
|
270
|
+
|
|
271
|
+
Open the [MCP Jam web inspector](https://www.mcpjam.com), go to **Connect**,
|
|
272
|
+
and select **Add Server**:
|
|
273
|
+
- **Server Name** — `xaa-quick-start`
|
|
274
|
+
- **Connection Type** — `HTTPS`, with the route URL
|
|
275
|
+
`https://your-gateway.zuplo.dev/mcp/todo0`
|
|
276
|
+
- **Authentication** — `OAuth`
|
|
277
|
+
|
|
278
|
+
<ModalScreenshot>
|
|
279
|
+
|
|
280
|
+

|
|
281
|
+
|
|
282
|
+
</ModalScreenshot>
|
|
283
|
+
|
|
284
|
+
Select **Add Server**. MCP Jam starts the OAuth flow and redirects you to
|
|
285
|
+
sign in. Sign in at the IdenX screen (any email, no password) and approve the
|
|
286
|
+
consent screen. MCP Jam returns to the inspector and the server shows as
|
|
287
|
+
**Connected**.
|
|
288
|
+
|
|
289
|
+
<Framed>
|
|
290
|
+
|
|
291
|
+

|
|
292
|
+
|
|
293
|
+
</Framed>
|
|
294
|
+
|
|
295
|
+
Open the **Resources** panel and select **List all todos**. The todos come
|
|
296
|
+
back as JSON — the agent never touched the XAA protocol. Behind the scenes
|
|
297
|
+
the gateway minted an ID-JAG from the playground IdP, redeemed it at the
|
|
298
|
+
resource authorization server, and called the Todo0 MCP server with the
|
|
299
|
+
resulting access token.
|
|
300
|
+
|
|
301
|
+
<Framed>
|
|
302
|
+
|
|
303
|
+

|
|
304
|
+
|
|
305
|
+
</Framed>
|
|
306
|
+
|
|
307
|
+
</Stepper>
|
|
308
|
+
|
|
309
|
+
## Verify from the command line (optional)
|
|
310
|
+
|
|
311
|
+
To confirm the gateway is deployed and secured without a client, check its OAuth
|
|
312
|
+
metadata and the protected route:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# Protected-resource metadata is published for the route (expect 200)
|
|
316
|
+
curl -s https://your-gateway.zuplo.dev/.well-known/oauth-protected-resource/mcp/todo0
|
|
317
|
+
|
|
318
|
+
# The MCP route rejects unauthenticated calls (expect 401)
|
|
319
|
+
curl -s -o /dev/null -w "%{http_code}\n" -X POST \
|
|
320
|
+
https://your-gateway.zuplo.dev/mcp/todo0 \
|
|
321
|
+
-H "Content-Type: application/json" \
|
|
322
|
+
-H "Accept: application/json, text/event-stream" \
|
|
323
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1.0.0"}}}'
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
## Next steps
|
|
327
|
+
|
|
328
|
+
- [Configuration reference](./policy-reference.mdx) — the full `idJag` option
|
|
329
|
+
set.
|
|
330
|
+
- [Overview](./overview.mdx) — the protocol and the gateway's role explained.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zuplo",
|
|
3
|
-
"version": "6.71.
|
|
3
|
+
"version": "6.71.13",
|
|
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.71.
|
|
23
|
-
"@zuplo/core": "6.71.
|
|
24
|
-
"@zuplo/runtime": "6.71.
|
|
22
|
+
"@zuplo/cli": "6.71.13",
|
|
23
|
+
"@zuplo/core": "6.71.13",
|
|
24
|
+
"@zuplo/runtime": "6.71.13",
|
|
25
25
|
"@zuplo/test": "1.4.0"
|
|
26
26
|
}
|
|
27
27
|
}
|