zuplo 6.70.56 → 6.70.59

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.
@@ -0,0 +1,288 @@
1
+ ---
2
+ title: "Curate the tools an upstream exposes (in code)"
3
+ sidebar_label: "Curate tools (in code)"
4
+ description:
5
+ Restrict and re-project the tools, prompts, resources, and resource templates
6
+ a Zuplo MCP Gateway route exposes from its upstream MCP server by configuring
7
+ the mcp-capability-filter-inbound policy in code.
8
+ ---
9
+
10
+ <CurateToolsPicker
11
+ mode="local"
12
+ alternateLink="/mcp-gateway/how-to/curate-tools"
13
+ />
14
+
15
+ When an upstream MCP server exposes more capabilities than belong in front of an
16
+ AI client, attach the `mcp-capability-filter-inbound` policy to the route to
17
+ allow-list the subset that should pass through, override descriptions or
18
+ annotations, and block direct calls to anything outside the list.
19
+
20
+ For the conceptual model behind capability filtering, including what the policy
21
+ filters, the omit-versus-empty-array rule, and how projections are merged, see
22
+ [Capability filtering](../capability-filtering.mdx).
23
+
24
+ Prefer the Portal? The [Portal version](./curate-tools.mdx) reaches the same
25
+ result from the MCP Gateway Virtual Server UI.
26
+
27
+ ## Add the capability filter policy
28
+
29
+ 1. Declare the policy in `config/policies.json`. List the upstream identifiers
30
+ you want to expose for each capability type (`name` for tools and prompts,
31
+ `uri` for resources, `uriTemplate` for resource templates):
32
+
33
+ ```jsonc title="config/policies.json"
34
+ {
35
+ "name": "filter-linear-tools",
36
+ "policyType": "mcp-capability-filter-inbound",
37
+ "handler": {
38
+ "module": "$import(@zuplo/runtime/mcp-gateway)",
39
+ "export": "McpCapabilityFilterInboundPolicy",
40
+ "options": {
41
+ "tools": ["list_issues", "get_issue", "create_issue"],
42
+ },
43
+ },
44
+ }
45
+ ```
46
+
47
+ 2. Attach the policy to the route in `config/routes.oas.json`, **after**
48
+ `mcp-token-exchange-inbound` so the filter operates on the final upstream
49
+ response:
50
+
51
+ ```jsonc title="config/routes.oas.json"
52
+ "/mcp/linear-v1": {
53
+ "get,post": {
54
+ "operationId": "linear-mcp-server",
55
+ "x-zuplo-route": {
56
+ "corsPolicy": "none",
57
+ "handler": {
58
+ "module": "$import(@zuplo/runtime/mcp-gateway)",
59
+ "export": "McpProxyHandler",
60
+ "options": { "rewritePattern": "https://mcp.linear.app/mcp" }
61
+ },
62
+ "policies": {
63
+ "inbound": [
64
+ "auth0-managed-oauth",
65
+ "mcp-token-exchange-linear",
66
+ "filter-linear-tools"
67
+ ]
68
+ }
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ Because `prompts`, `resources`, and `resourceTemplates` are omitted from the
75
+ options, the upstream's prompts and resources flow through unmodified. Only the
76
+ tool list is restricted.
77
+
78
+ ## Override a tool description
79
+
80
+ To rewrite the description or annotations a client sees while keeping the
81
+ upstream identifier as the match key, replace the string entry with a projection
82
+ object:
83
+
84
+ ```jsonc
85
+ {
86
+ "options": {
87
+ "tools": [
88
+ {
89
+ "name": "create_issue",
90
+ "description": "Create a Linear issue. Provide a title and team; everything else is optional.",
91
+ },
92
+ "list_issues",
93
+ "get_issue",
94
+ ],
95
+ },
96
+ }
97
+ ```
98
+
99
+ The string entries (`"list_issues"`, `"get_issue"`) pass through with the
100
+ upstream's own descriptions. The projection object overrides `create_issue`'s
101
+ description while keeping the upstream's input schema, output schema, and `name`
102
+ untouched.
103
+
104
+ ## Override tool annotations
105
+
106
+ [Tool annotations](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)
107
+ are deep-merged with the upstream's annotations, so fields you specify win and
108
+ fields you don't specify pass through. The same applies to `_meta`:
109
+
110
+ ```jsonc
111
+ {
112
+ "tools": [
113
+ {
114
+ "name": "delete_issue",
115
+ "description": "Delete a Linear issue. This is irreversible.",
116
+ "annotations": {
117
+ "destructiveHint": true,
118
+ "readOnlyHint": false,
119
+ },
120
+ "_meta": {
121
+ "io.example.audit": "high",
122
+ },
123
+ },
124
+ ],
125
+ }
126
+ ```
127
+
128
+ ## Project a resource
129
+
130
+ Resources use `uri` as the match key. A resource projection can rewrite the
131
+ downstream-facing `name`, `description`, or `mimeType`:
132
+
133
+ ```jsonc
134
+ {
135
+ "resources": [
136
+ {
137
+ "uri": "stripe://customers",
138
+ "name": "Customers",
139
+ "description": "All Stripe customers visible to this account.",
140
+ "mimeType": "application/json",
141
+ },
142
+ ],
143
+ "resourceTemplates": [
144
+ {
145
+ "uriTemplate": "stripe://customers/{id}",
146
+ "name": "Customer detail",
147
+ "description": "A single Stripe customer keyed by ID.",
148
+ },
149
+ ],
150
+ }
151
+ ```
152
+
153
+ ## Block everything from a capability type
154
+
155
+ Provide an empty array to expose nothing of that type. The list response becomes
156
+ empty and every direct call returns `MethodNotFound`:
157
+
158
+ ```jsonc
159
+ {
160
+ "options": {
161
+ "tools": ["safe_tool_a", "safe_tool_b"],
162
+ "prompts": [],
163
+ "resources": [],
164
+ "resourceTemplates": [],
165
+ },
166
+ }
167
+ ```
168
+
169
+ To turn a route into a temporary kill switch, with all capability types disabled
170
+ without removing the route from configuration, set every type to `[]`:
171
+
172
+ ```jsonc
173
+ {
174
+ "options": {
175
+ "tools": [],
176
+ "prompts": [],
177
+ "resources": [],
178
+ "resourceTemplates": [],
179
+ },
180
+ }
181
+ ```
182
+
183
+ :::caution
184
+
185
+ Omitting an option behaves like a pass-through; an empty array (`"tools": []`)
186
+ hides every capability of that type. Confusing the two is the most common source
187
+ of "why can the client still see that tool?" reports.
188
+
189
+ :::
190
+
191
+ ## Example: read-only Linear
192
+
193
+ Suppose the corp Linear upstream exposes more than two dozen tools and only the
194
+ read-only subset belongs in front of the team's AI assistant. Allow-list the
195
+ read tools, override descriptions for clarity, and hide all prompts and
196
+ resources:
197
+
198
+ ```jsonc title="config/policies.json"
199
+ {
200
+ "name": "filter-linear-read-only",
201
+ "policyType": "mcp-capability-filter-inbound",
202
+ "handler": {
203
+ "module": "$import(@zuplo/runtime/mcp-gateway)",
204
+ "export": "McpCapabilityFilterInboundPolicy",
205
+ "options": {
206
+ "tools": [
207
+ {
208
+ "name": "list_issues",
209
+ "description": "List Linear issues. Filter by team, state, assignee, or label.",
210
+ },
211
+ {
212
+ "name": "get_issue",
213
+ "description": "Get a single Linear issue by ID or identifier (e.g. ENG-123).",
214
+ },
215
+ {
216
+ "name": "list_teams",
217
+ "description": "List the teams in the current Linear workspace.",
218
+ },
219
+ {
220
+ "name": "list_projects",
221
+ "description": "List the projects in the current Linear workspace.",
222
+ "annotations": {
223
+ "readOnlyHint": true,
224
+ },
225
+ },
226
+ ],
227
+ "prompts": [],
228
+ "resources": [],
229
+ "resourceTemplates": [],
230
+ },
231
+ },
232
+ }
233
+ ```
234
+
235
+ Attach the policy to a dedicated route in `config/routes.oas.json`:
236
+
237
+ ```jsonc title="config/routes.oas.json"
238
+ "/mcp/linear-readonly": {
239
+ "get,post": {
240
+ "operationId": "linear-readonly-mcp-server",
241
+ "x-zuplo-route": {
242
+ "corsPolicy": "none",
243
+ "handler": {
244
+ "module": "$import(@zuplo/runtime/mcp-gateway)",
245
+ "export": "McpProxyHandler",
246
+ "options": { "rewritePattern": "https://mcp.linear.app/mcp" }
247
+ },
248
+ "policies": {
249
+ "inbound": [
250
+ "auth0-managed-oauth",
251
+ "mcp-token-exchange-linear",
252
+ "filter-linear-read-only"
253
+ ]
254
+ }
255
+ }
256
+ }
257
+ }
258
+ ```
259
+
260
+ The same upstream Linear MCP server is now reachable at two routes, the
261
+ full-featured `/mcp/linear-v1` and the curated `/mcp/linear-readonly`, each with
262
+ its own surface area.
263
+
264
+ ## Verify the filter
265
+
266
+ After deploying (or restarting `zuplo dev`), confirm the filter is active:
267
+
268
+ 1. Connect a test client (the [MCP Inspector](../test-clients.mdx#mcp-inspector)
269
+ is the fastest option) to the filtered route.
270
+ 2. Call `tools/list`. Only the allow-listed tools should appear.
271
+ 3. Call `tools/call` with a tool name that isn't on the list. The gateway
272
+ returns a JSON-RPC `MethodNotFound` error before the request reaches the
273
+ upstream.
274
+
275
+ If a tool you expected to see doesn't appear, check the upstream's `tools/list`
276
+ response directly. The match is case-sensitive and exact, so a typo or
277
+ capitalization difference makes the entry not match.
278
+
279
+ ## Related
280
+
281
+ - [Curate tools in the Portal](./curate-tools.mdx): do the same from the Virtual
282
+ Server UI.
283
+ - [Capability filtering](../capability-filtering.mdx): the conceptual model
284
+ behind the policy.
285
+ - [`McpProxyHandler` reference](../code-config/mcp-proxy-handler.mdx): the route
286
+ handler the filter runs in front of.
287
+ - [Connect a gateway to an upstream OAuth provider](./connect-upstream-oauth.mdx):
288
+ pair the filter with per-user upstream OAuth.
@@ -2,277 +2,123 @@
2
2
  title: "Curate the tools an upstream exposes"
3
3
  sidebar_label: "Curate tools"
4
4
  description:
5
- Restrict and re-project the tools, prompts, resources, and resource templates
6
- a Zuplo MCP Gateway route exposes from its upstream MCP server using the
7
- mcp-capability-filter-inbound policy.
5
+ Restrict which tools, prompts, and resources a Zuplo MCP Gateway Virtual
6
+ Server exposes from its upstream MCP server, using the Curate option in the
7
+ Portal wizard.
8
8
  ---
9
9
 
10
- When an upstream MCP server exposes more capabilities than belong in front of an
11
- AI client, attach the `mcp-capability-filter-inbound` policy to the route to
12
- allow-list the subset that should pass through, override descriptions or
13
- annotations, and block direct calls to anything outside the list.
10
+ <CurateToolsPicker
11
+ mode="portal"
12
+ alternateLink="/mcp-gateway/how-to/curate-tools-local"
13
+ />
14
14
 
15
- For the conceptual model behind capability filtering what the policy filters,
16
- the omit-versus-empty-array rule, and how projections are merged see
15
+ When an upstream MCP server exposes more capabilities than belong in front of an
16
+ AI client, curate the subset that passes through. In the Portal, the **MCP
17
+ Gateway Virtual Server** wizard does this on its **Tools** step: choose
18
+ **Curate** instead of **Passthrough** and pick exactly what to expose. The
19
+ wizard writes an `mcp-capability-filter-inbound` policy and attaches it to the
20
+ route for you.
21
+
22
+ For the conceptual model behind capability filtering, including what it filters
23
+ and how projections work, see
17
24
  [Capability filtering](../capability-filtering.mdx).
18
25
 
19
- ## Add the capability filter policy
20
-
21
- 1. Declare the policy in `config/policies.json`. List the upstream identifiers
22
- you want to expose for each capability type — `name` for tools and prompts,
23
- `uri` for resources, `uriTemplate` for resource templates:
24
-
25
- ```jsonc title="config/policies.json"
26
- {
27
- "name": "filter-linear-tools",
28
- "policyType": "mcp-capability-filter-inbound",
29
- "handler": {
30
- "module": "$import(@zuplo/runtime/mcp-gateway)",
31
- "export": "McpCapabilityFilterInboundPolicy",
32
- "options": {
33
- "tools": ["list_issues", "get_issue", "create_issue"],
34
- },
35
- },
36
- }
37
- ```
38
-
39
- 2. Attach the policy to the route in `config/routes.oas.json`, **after**
40
- `mcp-token-exchange-inbound` so the filter operates on the final upstream
41
- response:
42
-
43
- ```jsonc title="config/routes.oas.json"
44
- "/mcp/linear-v1": {
45
- "get,post": {
46
- "operationId": "linear-mcp-server",
47
- "x-zuplo-route": {
48
- "corsPolicy": "none",
49
- "handler": {
50
- "module": "$import(@zuplo/runtime/mcp-gateway)",
51
- "export": "McpProxyHandler",
52
- "options": { "rewritePattern": "https://mcp.linear.app/mcp" }
53
- },
54
- "policies": {
55
- "inbound": [
56
- "auth0-managed-oauth",
57
- "mcp-token-exchange-linear",
58
- "filter-linear-tools"
59
- ]
60
- }
61
- }
62
- }
63
- }
64
- ```
65
-
66
- Because `prompts`, `resources`, and `resourceTemplates` are omitted from the
67
- options, the upstream's prompts and resources flow through unmodified. Only the
68
- tool list is restricted.
69
-
70
- ## Override a tool description
71
-
72
- To rewrite the description or annotations a client sees while keeping the
73
- upstream identifier as the match key, replace the string entry with a projection
74
- object:
75
-
76
- ```jsonc
77
- {
78
- "options": {
79
- "tools": [
80
- {
81
- "name": "create_issue",
82
- "description": "Create a Linear issue. Provide a title and team; everything else is optional.",
83
- },
84
- "list_issues",
85
- "get_issue",
86
- ],
87
- },
88
- }
89
- ```
90
-
91
- The string entries (`"list_issues"`, `"get_issue"`) pass through with the
92
- upstream's own descriptions. The projection object overrides `create_issue`'s
93
- description while keeping the upstream's input schema, output schema, and `name`
94
- untouched.
95
-
96
- ## Override tool annotations
97
-
98
- [Tool annotations](https://modelcontextprotocol.io/specification/2025-11-25/server/tools)
99
- are deep-merged with the upstream's annotations — fields you specify win, fields
100
- you don't specify pass through. The same applies to `_meta`:
101
-
102
- ```jsonc
103
- {
104
- "tools": [
105
- {
106
- "name": "delete_issue",
107
- "description": "Delete a Linear issue. This is irreversible.",
108
- "annotations": {
109
- "destructiveHint": true,
110
- "readOnlyHint": false,
111
- },
112
- "_meta": {
113
- "io.example.audit": "high",
114
- },
115
- },
116
- ],
117
- }
118
- ```
119
-
120
- ## Project a resource
121
-
122
- Resources use `uri` as the match key. A resource projection can rewrite the
123
- downstream-facing `name`, `description`, or `mimeType`:
124
-
125
- ```jsonc
126
- {
127
- "resources": [
128
- {
129
- "uri": "stripe://customers",
130
- "name": "Customers",
131
- "description": "All Stripe customers visible to this account.",
132
- "mimeType": "application/json",
133
- },
134
- ],
135
- "resourceTemplates": [
136
- {
137
- "uriTemplate": "stripe://customers/{id}",
138
- "name": "Customer detail",
139
- "description": "A single Stripe customer keyed by ID.",
140
- },
141
- ],
142
- }
143
- ```
144
-
145
- ## Block everything from a capability type
146
-
147
- Provide an empty array to expose nothing of that type. The list response becomes
148
- empty and every direct call returns `MethodNotFound`:
149
-
150
- ```jsonc
151
- {
152
- "options": {
153
- "tools": ["safe_tool_a", "safe_tool_b"],
154
- "prompts": [],
155
- "resources": [],
156
- "resourceTemplates": [],
157
- },
158
- }
159
- ```
160
-
161
- To turn a route into a temporary kill switch — all capability types disabled
162
- without removing the route from configuration — set every type to `[]`:
163
-
164
- ```jsonc
165
- {
166
- "options": {
167
- "tools": [],
168
- "prompts": [],
169
- "resources": [],
170
- "resourceTemplates": [],
171
- },
172
- }
173
- ```
174
-
175
- :::caution
176
-
177
- Omitting an option behaves like a pass-through; an empty array (`"tools": []`)
178
- hides every capability of that type. Confusing the two is the most common source
179
- of "why can the client still see that tool?" reports.
26
+ Prefer working in code? The [code version](./curate-tools-local.mdx) configures
27
+ the same policy directly in your project files.
28
+
29
+ ## Curate on the Tools step
30
+
31
+ The **Tools** step appears while you add or edit an MCP Gateway Virtual Server.
32
+ For a full walkthrough of creating one, see the
33
+ [Portal quickstart](../quickstart.mdx).
34
+
35
+ 1. **Open the Virtual Server wizard.** On the **Code** tab, click **Add Route**
36
+ and choose **MCP Gateway Virtual Server**, then work through the wizard to
37
+ the **Tools** step. (To curate an existing server, open its route and reopen
38
+ the wizard.)
39
+
40
+ 2. **Choose Curate.** The Tools step offers two modes:
41
+ - **Passthrough** federates the upstream's full catalog live. Zero config,
42
+ and the safest default when you want to expose everything.
43
+ - **Curate** lets you select the specific tools, prompts, and resources to
44
+ expose. Everything you don't select is hidden from clients.
45
+
46
+ Select **Curate**.
47
+
48
+ <ModalScreenshot size="md">
49
+
50
+ ![Choose Passthrough or Curate on the Tools step](../../../public/media/mcp-gateway-quickstart/04-tools.png)
51
+
52
+ </ModalScreenshot>
53
+
54
+ 3. **Pick what to expose.** Choosing Curate prompts you to sign in to the
55
+ upstream service so the wizard can read its catalog. After you sign in, the
56
+ **Upstream catalog** lists the upstream's tools, prompts, and resources,
57
+ grouped by category (for example, a **Read-only** group for tools that don't
58
+ modify state), each with its description and a checkbox.
59
+
60
+ Clear the checkbox next to anything clients shouldn't see, or toggle a whole
61
+ group at once with its group checkbox. For example, keep the **Read-only**
62
+ group and clear the write or destructive tools. Anything left unselected is
63
+ blocked at the gateway.
64
+
65
+ <ModalScreenshot size="md">
66
+
67
+ ![Curate the upstream catalog by selecting tools to expose](../../../public/media/mcp-gateway-quickstart/07-curate-tools.png)
68
+
69
+ </ModalScreenshot>
70
+
71
+ 4. **Finish the wizard and save.** The wizard adds the
72
+ `mcp-capability-filter-inbound` policy to `config/policies.json` and wires it
73
+ into the route. **Save** the project to deploy the change.
74
+
75
+ :::note
76
+
77
+ Passthrough needs no upstream sign-in, since it federates the catalog live
78
+ instead of reading it up front.
180
79
 
181
80
  :::
182
81
 
183
- ## Worked example: read-only Linear
184
-
185
- Suppose the corp Linear upstream exposes more than two dozen tools and only the
186
- read-only subset belongs in front of the team's AI assistant. Allow-list the
187
- read tools, override descriptions for clarity, and hide all prompts and
188
- resources:
189
-
190
- ```jsonc title="config/policies.json"
191
- {
192
- "name": "filter-linear-read-only",
193
- "policyType": "mcp-capability-filter-inbound",
194
- "handler": {
195
- "module": "$import(@zuplo/runtime/mcp-gateway)",
196
- "export": "McpCapabilityFilterInboundPolicy",
197
- "options": {
198
- "tools": [
199
- {
200
- "name": "list_issues",
201
- "description": "List Linear issues. Filter by team, state, assignee, or label.",
202
- },
203
- {
204
- "name": "get_issue",
205
- "description": "Get a single Linear issue by ID or identifier (e.g. ENG-123).",
206
- },
207
- {
208
- "name": "list_teams",
209
- "description": "List the teams in the current Linear workspace.",
210
- },
211
- {
212
- "name": "list_projects",
213
- "description": "List the projects in the current Linear workspace.",
214
- "annotations": {
215
- "readOnlyHint": true,
216
- },
217
- },
218
- ],
219
- "prompts": [],
220
- "resources": [],
221
- "resourceTemplates": [],
222
- },
223
- },
224
- }
225
- ```
226
-
227
- Attach the policy to a dedicated route in `config/routes.oas.json`:
228
-
229
- ```jsonc title="config/routes.oas.json"
230
- "/mcp/linear-readonly": {
231
- "get,post": {
232
- "operationId": "linear-readonly-mcp-server",
233
- "x-zuplo-route": {
234
- "corsPolicy": "none",
235
- "handler": {
236
- "module": "$import(@zuplo/runtime/mcp-gateway)",
237
- "export": "McpProxyHandler",
238
- "options": { "rewritePattern": "https://mcp.linear.app/mcp" }
239
- },
240
- "policies": {
241
- "inbound": [
242
- "auth0-managed-oauth",
243
- "mcp-token-exchange-linear",
244
- "filter-linear-read-only"
245
- ]
246
- }
247
- }
248
- }
249
- }
250
- ```
251
-
252
- The same upstream Linear MCP server is now reachable at two routes — the
253
- full-featured `/mcp/linear-v1` and the curated `/mcp/linear-readonly` — each
254
- with its own surface area.
82
+ ## What curation does at the gateway
83
+
84
+ Only what you select is exposed. Clients can't see or call anything else, so
85
+ unselected tools are blocked at the gateway before the request reaches the
86
+ upstream.
87
+
88
+ ## Go further in code
89
+
90
+ The wizard covers the common case: pick what to expose. For finer control, edit
91
+ the generated `mcp-capability-filter-inbound` policy directly. In code you can
92
+ also:
93
+
94
+ - **Override a tool's description or annotations** while keeping the upstream's
95
+ schemas and name.
96
+ - **Re-project a resource's** downstream-facing `name`, `description`, or
97
+ `mimeType`.
98
+ - **Block an entire capability type** as a temporary kill switch.
99
+
100
+ See the [code version](./curate-tools-local.mdx) for these projections and the
101
+ full policy reference.
255
102
 
256
103
  ## Verify the filter
257
104
 
258
- After deploying (or restarting `zuplo dev`), confirm the filter is active:
105
+ After saving (and the deploy completes), confirm the filter is active:
259
106
 
260
107
  1. Connect a test client (the [MCP Inspector](../test-clients.mdx#mcp-inspector)
261
- is the fastest option) to the filtered route.
262
- 2. Call `tools/list`. Only the allow-listed tools should appear.
263
- 3. Call `tools/call` with a tool name that isn't on the list. The gateway
264
- returns a JSON-RPC `MethodNotFound` error before the request reaches the
265
- upstream.
108
+ is the fastest option) to the curated route.
109
+ 2. Call `tools/list`. Only the tools you selected should appear.
110
+ 3. Call `tools/call` with a tool you didn't select. The gateway returns a
111
+ JSON-RPC `MethodNotFound` error before the request reaches the upstream.
266
112
 
267
- If a tool you expected to see doesn't appear, check the upstream's `tools/list`
268
- response directly — the match is case-sensitive and exact, so a typo or
269
- capitalization difference makes the entry not match.
113
+ If a tool you expected is missing, reopen the wizard's Tools step and confirm it
114
+ is selected.
270
115
 
271
116
  ## Related
272
117
 
273
- - [Capability filtering](../capability-filtering.mdx) the conceptual model
274
- behind the policy.
275
- - [`McpProxyHandler` reference](../code-config/mcp-proxy-handler.mdx) the
276
- route handler the filter runs in front of.
277
- - [Connect a gateway to an upstream OAuth provider](./connect-upstream-oauth.mdx)
278
- pair the filter with per-user upstream OAuth.
118
+ - [Curate tools in code](./curate-tools-local.mdx): configure the policy
119
+ directly, with description and annotation overrides.
120
+ - [Capability filtering](../capability-filtering.mdx): the conceptual model
121
+ behind curation.
122
+ - [Portal quickstart](../quickstart.mdx): create a Virtual Server end to end.
123
+ - [Connect a gateway to an upstream OAuth provider](./connect-upstream-oauth.mdx):
124
+ pair curation with per-user upstream OAuth.