zuplo 6.70.57 → 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.
- package/docs/articles/limits.mdx +1 -1
- package/docs/articles/mcp-quickstart-local.mdx +99 -0
- package/docs/articles/mcp-quickstart.mdx +2 -2
- package/docs/articles/step-1-setup-basic-gateway-local.mdx +60 -84
- package/docs/articles/step-2-add-rate-limiting-local.mdx +46 -77
- package/docs/articles/step-2-add-rate-limiting.mdx +0 -5
- package/docs/articles/step-3-add-api-key-auth-local.mdx +80 -130
- package/docs/articles/step-3-add-api-key-auth.mdx +0 -5
- package/docs/articles/step-4-deploying-to-the-edge-local.mdx +106 -0
- package/docs/articles/step-5-dynamic-rate-limiting-local.mdx +180 -0
- package/docs/articles/support.mdx +8 -8
- package/docs/dev-portal/zudoku/components/callout.mdx +126 -8
- package/docs/dev-portal/zudoku/components/head.mdx +2 -12
- package/docs/dev-portal/zudoku/customization/colors-theme.mdx +19 -0
- package/docs/dev-portal/zudoku/markdown/admonitions.md +79 -0
- package/docs/mcp-gateway/code-config/overview.mdx +13 -28
- package/docs/mcp-gateway/how-to/connect-upstream-api-key.mdx +146 -0
- package/docs/mcp-gateway/how-to/curate-tools-local.mdx +288 -0
- package/docs/mcp-gateway/how-to/curate-tools.mdx +103 -257
- package/docs/mcp-gateway/quickstart-local.mdx +291 -0
- package/docs/mcp-gateway/quickstart.mdx +208 -189
- package/docs/policies/monetization-inbound/schema.json +11 -1
- package/package.json +4 -4
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
<CurateToolsPicker
|
|
11
|
+
mode="portal"
|
|
12
|
+
alternateLink="/mcp-gateway/how-to/curate-tools-local"
|
|
13
|
+
/>
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
the
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+

|
|
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
|
+

|
|
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
|
-
##
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
|
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
|
|
262
|
-
2. Call `tools/list`. Only the
|
|
263
|
-
3. Call `tools/call` with a tool
|
|
264
|
-
|
|
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
|
|
268
|
-
|
|
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
|
-
- [
|
|
274
|
-
|
|
275
|
-
- [
|
|
276
|
-
|
|
277
|
-
- [
|
|
278
|
-
|
|
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.
|