zuplo 6.70.53 → 6.70.55

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.
Files changed (160) hide show
  1. package/docs/mcp-gateway/auth/configuring-auth0.mdx +216 -0
  2. package/docs/mcp-gateway/auth/configuring-clerk.mdx +153 -0
  3. package/docs/mcp-gateway/auth/configuring-cognito.mdx +128 -0
  4. package/docs/mcp-gateway/auth/configuring-entra.mdx +134 -0
  5. package/docs/mcp-gateway/auth/configuring-generic-oidc.mdx +242 -0
  6. package/docs/mcp-gateway/auth/configuring-google.mdx +117 -0
  7. package/docs/mcp-gateway/auth/configuring-keycloak.mdx +125 -0
  8. package/docs/mcp-gateway/auth/configuring-logto.mdx +116 -0
  9. package/docs/mcp-gateway/auth/configuring-okta.mdx +199 -0
  10. package/docs/mcp-gateway/auth/configuring-onelogin.mdx +122 -0
  11. package/docs/mcp-gateway/auth/configuring-ping.mdx +157 -0
  12. package/docs/mcp-gateway/auth/configuring-workos.mdx +117 -0
  13. package/docs/mcp-gateway/auth/manual-oauth-testing.mdx +528 -0
  14. package/docs/mcp-gateway/auth/overview.mdx +314 -0
  15. package/docs/mcp-gateway/auth/upstream-oauth.mdx +221 -0
  16. package/docs/mcp-gateway/capability-filtering.mdx +162 -0
  17. package/docs/mcp-gateway/code-config/compatibility-dates.mdx +33 -0
  18. package/docs/mcp-gateway/code-config/local-development.mdx +198 -0
  19. package/docs/mcp-gateway/code-config/mcp-proxy-handler.mdx +186 -0
  20. package/docs/mcp-gateway/code-config/multi-upstream.mdx +293 -0
  21. package/docs/mcp-gateway/code-config/overview.mdx +210 -0
  22. package/docs/mcp-gateway/connect-clients/chatgpt.mdx +127 -0
  23. package/docs/mcp-gateway/connect-clients/claude-code.mdx +184 -0
  24. package/docs/mcp-gateway/connect-clients/claude-desktop.mdx +160 -0
  25. package/docs/mcp-gateway/connect-clients/cursor.mdx +100 -0
  26. package/docs/mcp-gateway/connect-clients/other-clients.mdx +207 -0
  27. package/docs/mcp-gateway/connect-clients/overview.mdx +137 -0
  28. package/docs/mcp-gateway/connect-clients/vs-code.mdx +128 -0
  29. package/docs/mcp-gateway/how-it-works.mdx +266 -0
  30. package/docs/mcp-gateway/how-to/connect-upstream-oauth.mdx +268 -0
  31. package/docs/mcp-gateway/how-to/curate-tools.mdx +278 -0
  32. package/docs/mcp-gateway/introduction.mdx +151 -0
  33. package/docs/mcp-gateway/observability/analytics.mdx +191 -0
  34. package/docs/mcp-gateway/observability/logging.mdx +191 -0
  35. package/docs/mcp-gateway/quickstart.mdx +266 -0
  36. package/docs/mcp-gateway/reference.mdx +148 -0
  37. package/docs/mcp-gateway/test-clients.mdx +130 -0
  38. package/docs/mcp-gateway/troubleshooting.mdx +228 -0
  39. package/docs/mcp-server/introduction.mdx +10 -0
  40. package/docs/mcp-server/openai-apps-sdk.mdx +12 -0
  41. package/docs/policies/_index.md +14 -0
  42. package/docs/policies/akamai-ai-firewall/schema.json +1 -0
  43. package/docs/policies/akamai-firewall-for-ai-inbound/schema.json +1 -0
  44. package/docs/policies/akamai-firewall-for-ai-outbound/schema.json +1 -0
  45. package/docs/policies/amberflo-metering-inbound/schema.json +1 -0
  46. package/docs/policies/api-key-inbound/schema.json +1 -0
  47. package/docs/policies/audit-log-inbound/schema.json +1 -0
  48. package/docs/policies/auth0-jwt-auth-inbound/schema.json +1 -0
  49. package/docs/policies/authzen-inbound/schema.json +1 -0
  50. package/docs/policies/axiomatics-authz-inbound/schema.json +1 -0
  51. package/docs/policies/basic-auth-inbound/schema.json +1 -0
  52. package/docs/policies/bot-detection-inbound/schema.json +1 -0
  53. package/docs/policies/brownout-inbound/schema.json +1 -0
  54. package/docs/policies/caching-inbound/schema.json +1 -0
  55. package/docs/policies/change-method-inbound/schema.json +1 -0
  56. package/docs/policies/clear-headers-inbound/schema.json +1 -0
  57. package/docs/policies/clear-headers-outbound/schema.json +1 -0
  58. package/docs/policies/clerk-jwt-auth-inbound/schema.json +1 -0
  59. package/docs/policies/cognito-jwt-auth-inbound/schema.json +1 -0
  60. package/docs/policies/comet-opik-tracing-inbound/schema.json +1 -0
  61. package/docs/policies/complex-rate-limit-inbound/schema.json +1 -0
  62. package/docs/policies/composite-inbound/schema.json +1 -0
  63. package/docs/policies/composite-outbound/schema.json +1 -0
  64. package/docs/policies/curity-phantom-token-inbound/schema.json +1 -0
  65. package/docs/policies/firebase-jwt-inbound/schema.json +1 -0
  66. package/docs/policies/formdata-to-json-inbound/schema.json +1 -0
  67. package/docs/policies/galileo-tracing-inbound/schema.json +1 -0
  68. package/docs/policies/geo-filter-inbound/schema.json +1 -0
  69. package/docs/policies/graphql-complexity-limit-inbound/schema.json +1 -0
  70. package/docs/policies/graphql-disable-introspection-inbound/schema.json +1 -0
  71. package/docs/policies/graphql-introspection-filter-outbound/schema.json +1 -0
  72. package/docs/policies/http-deprecation-outbound/schema.json +1 -0
  73. package/docs/policies/jwt-scopes-inbound/schema.json +1 -0
  74. package/docs/policies/ldap-auth-inbound/schema.json +1 -0
  75. package/docs/policies/mcp-auth0-oauth-inbound/doc.md +54 -0
  76. package/docs/policies/mcp-auth0-oauth-inbound/intro.md +7 -0
  77. package/docs/policies/mcp-auth0-oauth-inbound/schema.json +135 -0
  78. package/docs/policies/mcp-capability-filter-inbound/doc.md +58 -0
  79. package/docs/policies/mcp-capability-filter-inbound/intro.md +9 -0
  80. package/docs/policies/mcp-capability-filter-inbound/schema.json +212 -0
  81. package/docs/policies/mcp-clerk-oauth-inbound/doc.md +34 -0
  82. package/docs/policies/mcp-clerk-oauth-inbound/intro.md +1 -0
  83. package/docs/policies/mcp-clerk-oauth-inbound/schema.json +134 -0
  84. package/docs/policies/mcp-cognito-oauth-inbound/doc.md +52 -0
  85. package/docs/policies/mcp-cognito-oauth-inbound/intro.md +7 -0
  86. package/docs/policies/mcp-cognito-oauth-inbound/schema.json +152 -0
  87. package/docs/policies/mcp-entra-oauth-inbound/doc.md +51 -0
  88. package/docs/policies/mcp-entra-oauth-inbound/intro.md +6 -0
  89. package/docs/policies/mcp-entra-oauth-inbound/schema.json +131 -0
  90. package/docs/policies/mcp-google-oauth-inbound/doc.md +52 -0
  91. package/docs/policies/mcp-google-oauth-inbound/intro.md +6 -0
  92. package/docs/policies/mcp-google-oauth-inbound/schema.json +125 -0
  93. package/docs/policies/mcp-keycloak-oauth-inbound/doc.md +43 -0
  94. package/docs/policies/mcp-keycloak-oauth-inbound/intro.md +2 -0
  95. package/docs/policies/mcp-keycloak-oauth-inbound/schema.json +140 -0
  96. package/docs/policies/mcp-logto-oauth-inbound/doc.md +52 -0
  97. package/docs/policies/mcp-logto-oauth-inbound/intro.md +6 -0
  98. package/docs/policies/mcp-logto-oauth-inbound/schema.json +131 -0
  99. package/docs/policies/mcp-oauth-inbound/doc.md +70 -0
  100. package/docs/policies/mcp-oauth-inbound/intro.md +11 -0
  101. package/docs/policies/mcp-oauth-inbound/schema.json +177 -0
  102. package/docs/policies/mcp-okta-oauth-inbound/doc.md +61 -0
  103. package/docs/policies/mcp-okta-oauth-inbound/intro.md +7 -0
  104. package/docs/policies/mcp-okta-oauth-inbound/schema.json +137 -0
  105. package/docs/policies/mcp-onelogin-oauth-inbound/doc.md +50 -0
  106. package/docs/policies/mcp-onelogin-oauth-inbound/intro.md +6 -0
  107. package/docs/policies/mcp-onelogin-oauth-inbound/schema.json +131 -0
  108. package/docs/policies/mcp-ping-oauth-inbound/doc.md +80 -0
  109. package/docs/policies/mcp-ping-oauth-inbound/intro.md +7 -0
  110. package/docs/policies/mcp-ping-oauth-inbound/schema.json +151 -0
  111. package/docs/policies/mcp-token-exchange-inbound/doc.md +135 -0
  112. package/docs/policies/mcp-token-exchange-inbound/intro.md +6 -0
  113. package/docs/policies/mcp-token-exchange-inbound/schema.json +134 -0
  114. package/docs/policies/mcp-workos-oauth-inbound/doc.md +50 -0
  115. package/docs/policies/mcp-workos-oauth-inbound/intro.md +6 -0
  116. package/docs/policies/mcp-workos-oauth-inbound/schema.json +125 -0
  117. package/docs/policies/mock-api-inbound/schema.json +1 -0
  118. package/docs/policies/moesif-inbound/schema.json +1 -0
  119. package/docs/policies/monetization-inbound/schema.json +1 -0
  120. package/docs/policies/mtls-auth-inbound/schema.json +1 -0
  121. package/docs/policies/okta-fga-authz-inbound/schema.json +1 -0
  122. package/docs/policies/okta-jwt-auth-inbound/schema.json +1 -0
  123. package/docs/policies/open-id-jwt-auth-inbound/schema.json +1 -0
  124. package/docs/policies/openfga-authz-inbound/schema.json +1 -0
  125. package/docs/policies/openmeter-inbound/schema.json +1 -0
  126. package/docs/policies/prompt-injection-outbound/schema.json +1 -0
  127. package/docs/policies/propel-auth-jwt-inbound/schema.json +1 -0
  128. package/docs/policies/query-param-to-header-inbound/schema.json +1 -0
  129. package/docs/policies/quota-inbound/schema.json +1 -0
  130. package/docs/policies/rate-limit-inbound/schema.json +1 -0
  131. package/docs/policies/readme-metrics-inbound/schema.json +1 -0
  132. package/docs/policies/remove-headers-inbound/schema.json +1 -0
  133. package/docs/policies/remove-headers-outbound/schema.json +1 -0
  134. package/docs/policies/remove-query-params-inbound/schema.json +1 -0
  135. package/docs/policies/replace-string-outbound/schema.json +1 -0
  136. package/docs/policies/request-size-limit-inbound/schema.json +1 -0
  137. package/docs/policies/request-validation-inbound/schema.json +1 -0
  138. package/docs/policies/require-origin-inbound/schema.json +1 -0
  139. package/docs/policies/secret-masking-outbound/schema.json +1 -0
  140. package/docs/policies/semantic-cache-inbound/schema.json +1 -0
  141. package/docs/policies/set-body-inbound/schema.json +1 -0
  142. package/docs/policies/set-headers-inbound/schema.json +1 -0
  143. package/docs/policies/set-headers-outbound/schema.json +1 -0
  144. package/docs/policies/set-query-params-inbound/schema.json +1 -0
  145. package/docs/policies/set-status-outbound/schema.json +1 -0
  146. package/docs/policies/set-upstream-api-key-inbound/schema.json +1 -0
  147. package/docs/policies/sleep-inbound/schema.json +1 -0
  148. package/docs/policies/stripe-webhook-verification-inbound/schema.json +1 -0
  149. package/docs/policies/supabase-jwt-auth-inbound/schema.json +1 -0
  150. package/docs/policies/upstream-azure-ad-service-auth-inbound/schema.json +1 -0
  151. package/docs/policies/upstream-firebase-admin-auth-inbound/schema.json +1 -0
  152. package/docs/policies/upstream-firebase-user-auth-inbound/schema.json +1 -0
  153. package/docs/policies/upstream-gcp-federated-auth-inbound/schema.json +1 -0
  154. package/docs/policies/upstream-gcp-jwt-inbound/schema.json +1 -0
  155. package/docs/policies/upstream-gcp-service-auth-inbound/schema.json +1 -0
  156. package/docs/policies/upstream-zuplo-jwt-auth-inbound/schema.json +1 -0
  157. package/docs/policies/validate-json-schema-inbound/schema.json +1 -0
  158. package/docs/policies/web-bot-auth-inbound/schema.json +1 -0
  159. package/docs/policies/xml-to-json-outbound/schema.json +1 -0
  160. package/package.json +4 -4
@@ -0,0 +1,528 @@
1
+ ---
2
+ title: "Manual OAuth testing"
3
+ sidebar_label: "Manual OAuth testing"
4
+ description:
5
+ Walk through every step of the MCP Gateway's downstream OAuth flow with curl,
6
+ openssl, and jq. Useful for debugging discovery, registration, authorize,
7
+ token exchange, refresh, and the first authenticated MCP request.
8
+ ---
9
+
10
+ When an MCP client's OAuth integration goes wrong, exercising the gateway's
11
+ endpoints by hand is the fastest way to figure out where. This guide walks every
12
+ step of the downstream OAuth flow using `curl`, `openssl`, and `jq`. Each step
13
+ shows the request, the shape of the response, and what to look for.
14
+
15
+ The flow being tested is the standard MCP authorization handshake: discovery →
16
+ registration → authorize → token → MCP request → refresh. Read the
17
+ [authentication overview](./overview.mdx) for the conceptual model first.
18
+
19
+ :::note
20
+
21
+ The user-consent step is browser-based — there's no scriptable way to complete
22
+ it from a terminal. Steps 4 through 6 show the URL to open in a browser and the
23
+ redirect to inspect; the rest of the flow runs in your terminal.
24
+
25
+ :::
26
+
27
+ ## Prerequisites
28
+
29
+ - `curl`, `jq`, `openssl`, and a Bash-compatible shell.
30
+ - A deployed MCP Gateway with an
31
+ [MCP OAuth policy](./overview.mdx#identity-providers) configured (Auth0, Okta,
32
+ Entra, Google, or any other supported IdP) and at least one `/mcp/{slug}`
33
+ route.
34
+ - A browser to complete the user-consent step.
35
+
36
+ Throughout this guide, replace:
37
+
38
+ - `GATEWAY` with your gateway origin (e.g., `https://gateway.example.com`).
39
+ - `SLUG` with the route slug (e.g., `linear-v1`).
40
+ - `REDIRECT_URI` with a redirect URL that you can monitor — for testing,
41
+ `http://localhost:8765/callback` works because the URL only needs to capture
42
+ the `code` query parameter.
43
+
44
+ ```bash
45
+ GATEWAY="https://gateway.example.com"
46
+ SLUG="linear-v1"
47
+ REDIRECT_URI="http://localhost:8765/callback"
48
+ ```
49
+
50
+ <Stepper>
51
+
52
+ 1. **Discover the protected resource.**
53
+
54
+ An unauthenticated request to an MCP route should return a `401` with a
55
+ `WWW-Authenticate` header that points at the per-route Protected Resource
56
+ Metadata document.
57
+
58
+ ```bash
59
+ curl -i -X POST "${GATEWAY}/mcp/${SLUG}" \
60
+ -H "content-type: application/json" \
61
+ -H "accept: application/json, text/event-stream" \
62
+ -d '{"jsonrpc":"2.0","id":"1","method":"ping"}'
63
+ ```
64
+
65
+ Expected response:
66
+
67
+ ```http
68
+ HTTP/1.1 401 Unauthorized
69
+ WWW-Authenticate: Bearer realm="OAuth",
70
+ resource_metadata="https://gateway.example.com/.well-known/oauth-protected-resource/mcp/linear-v1"
71
+ ```
72
+
73
+ If you get a 200 instead, the route isn't protected. Check that the MCP OAuth
74
+ policy is attached to the route in `routes.oas.json`.
75
+
76
+ Now fetch the PRM document:
77
+
78
+ ```bash
79
+ curl -s "${GATEWAY}/.well-known/oauth-protected-resource/mcp/${SLUG}" | jq
80
+ ```
81
+
82
+ Expected response shape:
83
+
84
+ ```json
85
+ {
86
+ "resource": "https://gateway.example.com/mcp/linear-v1",
87
+ "resource_name": "Linear MCP Proxy",
88
+ "authorization_servers": ["https://gateway.example.com/mcp/linear-v1"],
89
+ "bearer_methods_supported": ["header"],
90
+ "scopes_supported": ["mcp:tools"],
91
+ "mcp_protocol_version": "2025-11-25"
92
+ }
93
+ ```
94
+
95
+ The `authorization_servers` array tells the client where to find the AS
96
+ metadata. For the gateway, the AS lives under the same origin.
97
+
98
+ 1. **Discover the authorization server.**
99
+
100
+ Fetch the per-route AS metadata document.
101
+
102
+ ```bash
103
+ curl -s "${GATEWAY}/.well-known/oauth-authorization-server/mcp/${SLUG}" | jq
104
+ ```
105
+
106
+ Expected response shape (truncated to the fields you care about):
107
+
108
+ ```json
109
+ {
110
+ "issuer": "https://gateway.example.com/mcp/linear-v1",
111
+ "authorization_endpoint": "https://gateway.example.com/oauth/authorize/mcp/linear-v1",
112
+ "token_endpoint": "https://gateway.example.com/oauth/token",
113
+ "registration_endpoint": "https://gateway.example.com/oauth/register",
114
+ "revocation_endpoint": "https://gateway.example.com/oauth/revoke",
115
+ "scopes_supported": ["mcp:tools"],
116
+ "response_types_supported": ["code"],
117
+ "grant_types_supported": ["authorization_code", "refresh_token"],
118
+ "code_challenge_methods_supported": ["S256"],
119
+ "token_endpoint_auth_methods_supported": [
120
+ "none",
121
+ "client_secret_basic",
122
+ "client_secret_post",
123
+ "private_key_jwt"
124
+ ],
125
+ "client_id_metadata_document_supported": true
126
+ }
127
+ ```
128
+
129
+ Capture the URLs you'll need:
130
+
131
+ ```bash
132
+ AS_METADATA=$(curl -s "${GATEWAY}/.well-known/oauth-authorization-server/mcp/${SLUG}")
133
+ AUTH_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.authorization_endpoint')
134
+ TOKEN_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.token_endpoint')
135
+ REGISTRATION_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.registration_endpoint')
136
+ ```
137
+
138
+ If `code_challenge_methods_supported` doesn't include `S256`, something is
139
+ wrong with the gateway configuration. The spec requires `S256` and the
140
+ gateway always advertises it.
141
+
142
+ 1. **Register a client (DCR).**
143
+
144
+ For this test, register a public client with
145
+ `token_endpoint_auth_method: "none"`. This is the simplest mode and matches
146
+ what a CLI client would use.
147
+
148
+ ```bash
149
+ DCR_RESPONSE=$(curl -s -X POST "${REGISTRATION_ENDPOINT}" \
150
+ -H "content-type: application/json" \
151
+ -d "{
152
+ \"client_name\": \"Manual OAuth Test\",
153
+ \"redirect_uris\": [\"${REDIRECT_URI}\"],
154
+ \"grant_types\": [\"authorization_code\", \"refresh_token\"],
155
+ \"response_types\": [\"code\"],
156
+ \"token_endpoint_auth_method\": \"none\",
157
+ \"scope\": \"mcp:tools\"
158
+ }")
159
+
160
+ echo "$DCR_RESPONSE" | jq
161
+ CLIENT_ID=$(echo "$DCR_RESPONSE" | jq -r '.client_id')
162
+ ```
163
+
164
+ Expected response shape:
165
+
166
+ ```json
167
+ {
168
+ "client_id": "dcr:abc123...",
169
+ "client_id_issued_at": 1747958400,
170
+ "client_id_metadata_document_supported": true,
171
+ "client_name": "Manual OAuth Test",
172
+ "redirect_uris": ["http://localhost:8765/callback"],
173
+ "grant_types": ["authorization_code", "refresh_token"],
174
+ "response_types": ["code"],
175
+ "token_endpoint_auth_method": "none",
176
+ "scope": "mcp:tools"
177
+ }
178
+ ```
179
+
180
+ The client ID is opaque. DCR clients expire 90 days after issuance.
181
+
182
+ 1. **Build the authorize URL with PKCE.**
183
+
184
+ Generate a PKCE verifier and S256 challenge, plus a state value for CSRF.
185
+
186
+ ```bash
187
+ CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '\n=+/' | cut -c1-128)
188
+ CODE_CHALLENGE=$(printf "%s" "$CODE_VERIFIER" | openssl dgst -sha256 -binary \
189
+ | openssl base64 | tr '/+' '_-' | tr -d '=')
190
+ STATE=$(openssl rand -hex 16)
191
+ RESOURCE=$(echo "$AS_METADATA" | jq -r '.issuer')
192
+
193
+ echo "CODE_VERIFIER: $CODE_VERIFIER"
194
+ echo "CODE_CHALLENGE: $CODE_CHALLENGE"
195
+ echo "STATE: $STATE"
196
+ echo "RESOURCE: $RESOURCE"
197
+ ```
198
+
199
+ Build the authorize URL. The `resource` parameter is **required** by the MCP
200
+ spec on every authorization and token request.
201
+
202
+ ```bash
203
+ AUTH_URL="${AUTH_ENDPOINT}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&state=${STATE}&scope=mcp:tools&resource=$(printf %s "$RESOURCE" | jq -sRr @uri)"
204
+
205
+ echo "Open this URL in a browser:"
206
+ echo "$AUTH_URL"
207
+ ```
208
+
209
+ Open the URL in a browser. The flow is:
210
+ 1. The gateway redirects you to your IdP's login page.
211
+ 2. You authenticate at the IdP.
212
+ 3. The IdP redirects back to the gateway's `/oauth/callback`.
213
+ 4. The gateway renders the consent setup page.
214
+ 5. You click **Authorize**.
215
+ 6. The gateway redirects to your `redirect_uri` with `?code=...&state=...`.
216
+
217
+ Capture the `code` value from the final redirect URL. There's no listener on
218
+ `http://localhost:8765`, so the browser shows a connection-refused page —
219
+ that's expected. Copy the `code` value out of the address bar.
220
+
221
+ :::warning
222
+
223
+ The authorization code is single-use and short-lived (typically 30 seconds).
224
+ Run the next step immediately after copying it.
225
+
226
+ :::
227
+
228
+ ```bash
229
+ read -p "Enter the authorization code from the redirect URL: " AUTH_CODE
230
+ ```
231
+
232
+ 1. **Exchange the code for tokens.**
233
+
234
+ `POST /oauth/token` with the authorization-code grant. Public clients send
235
+ `client_id` in the form body; confidential clients use HTTP Basic.
236
+
237
+ ```bash
238
+ TOKEN_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
239
+ -H "content-type: application/x-www-form-urlencoded" \
240
+ --data-urlencode "grant_type=authorization_code" \
241
+ --data-urlencode "code=${AUTH_CODE}" \
242
+ --data-urlencode "redirect_uri=${REDIRECT_URI}" \
243
+ --data-urlencode "code_verifier=${CODE_VERIFIER}" \
244
+ --data-urlencode "client_id=${CLIENT_ID}" \
245
+ --data-urlencode "resource=${RESOURCE}")
246
+
247
+ echo "$TOKEN_RESPONSE" | jq
248
+
249
+ ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
250
+ REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.refresh_token')
251
+ ```
252
+
253
+ Expected response shape:
254
+
255
+ ```json
256
+ {
257
+ "access_token": "at_...",
258
+ "token_type": "Bearer",
259
+ "expires_in": 900,
260
+ "refresh_token": "rt_...",
261
+ "scope": "mcp:tools",
262
+ "resource": "https://gateway.example.com/mcp/linear-v1"
263
+ }
264
+ ```
265
+
266
+ A common failure mode here is `invalid_grant` because the authorization code
267
+ expired or was already used. Re-run from step 4.
268
+
269
+ Another common one is `invalid_request` if you forget the `code_verifier` or
270
+ omit the `resource` parameter.
271
+
272
+ 1. **Call the MCP endpoint with the access token.**
273
+
274
+ Now the access token can be presented as a bearer credential on the MCP
275
+ route.
276
+
277
+ ```bash
278
+ curl -s -X POST "${GATEWAY}/mcp/${SLUG}" \
279
+ -H "authorization: Bearer ${ACCESS_TOKEN}" \
280
+ -H "content-type: application/json" \
281
+ -H "accept: application/json, text/event-stream" \
282
+ -H "mcp-protocol-version: 2025-11-25" \
283
+ -d '{
284
+ "jsonrpc": "2.0",
285
+ "id": "1",
286
+ "method": "initialize",
287
+ "params": {
288
+ "protocolVersion": "2025-11-25",
289
+ "capabilities": {},
290
+ "clientInfo": {
291
+ "name": "manual-test",
292
+ "version": "0.0.0"
293
+ }
294
+ }
295
+ }' | jq
296
+ ```
297
+
298
+ Expected response is a JSON-RPC result with the upstream's `serverInfo` and
299
+ `capabilities`:
300
+
301
+ ```json
302
+ {
303
+ "jsonrpc": "2.0",
304
+ "id": "1",
305
+ "result": {
306
+ "protocolVersion": "2025-11-25",
307
+ "capabilities": {
308
+ "tools": {}
309
+ },
310
+ "serverInfo": {
311
+ "name": "linear",
312
+ "version": "..."
313
+ }
314
+ }
315
+ }
316
+ ```
317
+
318
+ If you see a JSON-RPC error with `code: -32042`
319
+ (`URLElicitationRequiredError`), the **upstream** MCP server requires OAuth
320
+ and the user hasn't connected to it yet. Open the `authUrl` in the error
321
+ payload's `data` field in a browser. See
322
+ [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx) for the full
323
+ flow.
324
+
325
+ If you see a `401`, the bearer token is missing, expired, revoked, or bound
326
+ to a different route — the response `WWW-Authenticate` header includes a
327
+ reason code via `error="..."`.
328
+
329
+ If you see a `403` with `error="insufficient_scope"`, the token has the wrong
330
+ scope. The gateway only issues `mcp:tools` today.
331
+
332
+ 1. **Refresh the access token.**
333
+
334
+ The access token expires in 15 minutes by default. Exchange the refresh token
335
+ for a new pair using the `refresh_token` grant.
336
+
337
+ ```bash
338
+ REFRESH_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
339
+ -H "content-type: application/x-www-form-urlencoded" \
340
+ --data-urlencode "grant_type=refresh_token" \
341
+ --data-urlencode "refresh_token=${REFRESH_TOKEN}" \
342
+ --data-urlencode "client_id=${CLIENT_ID}" \
343
+ --data-urlencode "resource=${RESOURCE}")
344
+
345
+ echo "$REFRESH_RESPONSE" | jq
346
+
347
+ ACCESS_TOKEN=$(echo "$REFRESH_RESPONSE" | jq -r '.access_token')
348
+ NEW_REFRESH_TOKEN=$(echo "$REFRESH_RESPONSE" | jq -r '.refresh_token')
349
+ ```
350
+
351
+ The refresh token rotates on every use. Presenting the old refresh token
352
+ again will **revoke the entire grant** — that's the spec's defense against
353
+ refresh-token replay. Always use the most recently issued refresh token.
354
+
355
+ The new access token can be used immediately on subsequent `/mcp/{slug}`
356
+ requests.
357
+
358
+ 1. **Revoke the tokens (optional cleanup).**
359
+
360
+ When you're done testing, revoke the grant.
361
+
362
+ ```bash
363
+ curl -s -i -X POST "${GATEWAY}/oauth/revoke" \
364
+ -H "content-type: application/x-www-form-urlencoded" \
365
+ --data-urlencode "token=${NEW_REFRESH_TOKEN}" \
366
+ --data-urlencode "token_type_hint=refresh_token" \
367
+ --data-urlencode "client_id=${CLIENT_ID}"
368
+ ```
369
+
370
+ Per RFC 7009, the gateway responds with `200 OK` and an empty body for both
371
+ successful revocations and unknown tokens. Subsequent MCP requests with the
372
+ revoked access token return `401`.
373
+
374
+ </Stepper>
375
+
376
+ ## Putting it all together
377
+
378
+ Here's a single Bash script that runs every step except the browser-based
379
+ authorize redirect. Save it as `test-oauth.sh` and run it after editing the
380
+ configuration block at the top.
381
+
382
+ ```bash
383
+ #!/usr/bin/env bash
384
+ # Manual OAuth flow test for the Zuplo MCP Gateway.
385
+ # Walks discovery → DCR → authorize URL → code exchange → MCP request → refresh.
386
+ # The authorize step is browser-based; the script pauses for you to paste the code.
387
+
388
+ set -euo pipefail
389
+
390
+ # ----- Configuration -----
391
+ GATEWAY="https://gateway.example.com"
392
+ SLUG="linear-v1"
393
+ REDIRECT_URI="http://localhost:8765/callback"
394
+ # -------------------------
395
+
396
+ echo "==> Step 1: discover protected resource"
397
+ PRM_URL="${GATEWAY}/.well-known/oauth-protected-resource/mcp/${SLUG}"
398
+ echo "PRM: ${PRM_URL}"
399
+ curl -s "${PRM_URL}" | jq -r '{authorization_servers, scopes_supported, mcp_protocol_version}'
400
+
401
+ echo
402
+ echo "==> Step 2: fetch AS metadata"
403
+ AS_METADATA=$(curl -s "${GATEWAY}/.well-known/oauth-authorization-server/mcp/${SLUG}")
404
+ AUTH_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.authorization_endpoint')
405
+ TOKEN_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.token_endpoint')
406
+ REGISTRATION_ENDPOINT=$(echo "$AS_METADATA" | jq -r '.registration_endpoint')
407
+ RESOURCE=$(echo "$AS_METADATA" | jq -r '.issuer')
408
+ echo "issuer: $RESOURCE"
409
+ echo "authorize: $AUTH_ENDPOINT"
410
+ echo "token: $TOKEN_ENDPOINT"
411
+
412
+ echo
413
+ echo "==> Step 3: register client (DCR)"
414
+ DCR_RESPONSE=$(curl -s -X POST "${REGISTRATION_ENDPOINT}" \
415
+ -H "content-type: application/json" \
416
+ -d "{
417
+ \"client_name\": \"Manual OAuth Test\",
418
+ \"redirect_uris\": [\"${REDIRECT_URI}\"],
419
+ \"grant_types\": [\"authorization_code\", \"refresh_token\"],
420
+ \"response_types\": [\"code\"],
421
+ \"token_endpoint_auth_method\": \"none\",
422
+ \"scope\": \"mcp:tools\"
423
+ }")
424
+ CLIENT_ID=$(echo "$DCR_RESPONSE" | jq -r '.client_id')
425
+ echo "client_id: $CLIENT_ID"
426
+
427
+ echo
428
+ echo "==> Step 4: build authorize URL with PKCE"
429
+ CODE_VERIFIER=$(openssl rand -base64 64 | tr -d '\n=+/' | cut -c1-128)
430
+ CODE_CHALLENGE=$(printf "%s" "$CODE_VERIFIER" | openssl dgst -sha256 -binary \
431
+ | openssl base64 | tr '/+' '_-' | tr -d '=')
432
+ STATE=$(openssl rand -hex 16)
433
+ RESOURCE_ENC=$(printf "%s" "$RESOURCE" | jq -sRr @uri)
434
+ AUTH_URL="${AUTH_ENDPOINT}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&code_challenge=${CODE_CHALLENGE}&code_challenge_method=S256&state=${STATE}&scope=mcp:tools&resource=${RESOURCE_ENC}"
435
+ echo
436
+ echo "Open this URL in a browser:"
437
+ echo "$AUTH_URL"
438
+ echo
439
+ echo "After completing login and consent, copy the 'code' query parameter from the redirect URL."
440
+ read -r -p "Enter the authorization code: " AUTH_CODE
441
+
442
+ echo
443
+ echo "==> Step 5: exchange code for tokens"
444
+ TOKEN_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
445
+ -H "content-type: application/x-www-form-urlencoded" \
446
+ --data-urlencode "grant_type=authorization_code" \
447
+ --data-urlencode "code=${AUTH_CODE}" \
448
+ --data-urlencode "redirect_uri=${REDIRECT_URI}" \
449
+ --data-urlencode "code_verifier=${CODE_VERIFIER}" \
450
+ --data-urlencode "client_id=${CLIENT_ID}" \
451
+ --data-urlencode "resource=${RESOURCE}")
452
+ echo "$TOKEN_RESPONSE" | jq
453
+ ACCESS_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.access_token')
454
+ REFRESH_TOKEN=$(echo "$TOKEN_RESPONSE" | jq -r '.refresh_token')
455
+
456
+ echo
457
+ echo "==> Step 6: call MCP endpoint with the access token"
458
+ curl -s -X POST "${GATEWAY}/mcp/${SLUG}" \
459
+ -H "authorization: Bearer ${ACCESS_TOKEN}" \
460
+ -H "content-type: application/json" \
461
+ -H "accept: application/json, text/event-stream" \
462
+ -H "mcp-protocol-version: 2025-11-25" \
463
+ -d '{
464
+ "jsonrpc": "2.0",
465
+ "id": "1",
466
+ "method": "initialize",
467
+ "params": {
468
+ "protocolVersion": "2025-11-25",
469
+ "capabilities": {},
470
+ "clientInfo": { "name": "manual-test", "version": "0.0.0" }
471
+ }
472
+ }' | jq
473
+
474
+ echo
475
+ echo "==> Step 7: refresh"
476
+ REFRESH_RESPONSE=$(curl -s -X POST "${TOKEN_ENDPOINT}" \
477
+ -H "content-type: application/x-www-form-urlencoded" \
478
+ --data-urlencode "grant_type=refresh_token" \
479
+ --data-urlencode "refresh_token=${REFRESH_TOKEN}" \
480
+ --data-urlencode "client_id=${CLIENT_ID}" \
481
+ --data-urlencode "resource=${RESOURCE}")
482
+ echo "$REFRESH_RESPONSE" | jq
483
+
484
+ echo
485
+ echo "Done."
486
+ ```
487
+
488
+ Make it executable and run it:
489
+
490
+ ```bash
491
+ chmod +x test-oauth.sh
492
+ ./test-oauth.sh
493
+ ```
494
+
495
+ ## Common issues
496
+
497
+ - **`401` on every MCP request after token exchange.** Token bound to a
498
+ different route than the one you're calling. Each token is scoped to one MCP
499
+ route. Either re-run for the intended route or call the route you authorized
500
+ for.
501
+ - **`401` with `error="invalid_token"` after a token reuse.** Refresh tokens
502
+ rotate on every use — presenting an old one revokes the entire grant. Re-run
503
+ the full flow.
504
+ - **`invalid_request` at the token endpoint.** Most often a missing `resource`
505
+ parameter or a missing `code_verifier`. Both are required.
506
+ - **`invalid_grant` at the token endpoint.** The authorization code expired or
507
+ was already redeemed. Re-run from step 4.
508
+ - **`invalid_audience`.** The bearer token is being used at a route whose
509
+ canonical resource URI doesn't match the token's `resource` claim. A
510
+ misconfigured custom domain or proxy can cause this.
511
+ - **The browser shows the gateway's consent page but the **Authorize** button is
512
+ disabled.** The route has an upstream that hasn't been connected yet. Click
513
+ the per-upstream **Connect** button first. See
514
+ [upstream OAuth](./upstream-oauth.mdx).
515
+ - **JSON-RPC error `-32042` (`URLElicitationRequiredError`).** The downstream
516
+ OAuth succeeded but the upstream MCP server requires OAuth and the user hasn't
517
+ connected. Open the `authUrl` in the error payload's `data` field in a
518
+ browser.
519
+
520
+ ## Related
521
+
522
+ - [Authentication overview](./overview.mdx) — the full
523
+ [identity provider catalog](./overview.mdx#identity-providers) and per-IdP
524
+ setup links.
525
+ - [Per-user OAuth to upstream MCP servers](./upstream-oauth.mdx)
526
+ - [Test clients](../test-clients.mdx) — exercise the same OAuth flow through the
527
+ MCP Inspector and MCPJam GUIs instead of `curl`.
528
+ - [MCP authorization spec, revision 2025-11-25](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization)