Skip to content

Commit 12ebcc3

Browse files
committed
feat: add support for MCP Protected Resource Metadata
* This makes it so that if there input OpenAPI description is defines a security requirement of oauth2 or openIdConnect type then, the API proxy will check to see that all the calls contain a bearer token in the authorization header. It does not actually check to see if the token is valid. The token is passed-through to the actual backend REST API.
1 parent 933657b commit 12ebcc3

File tree

7 files changed

+909
-10
lines changed

7 files changed

+909
-10
lines changed

examples/templates/mcp/apiproxy.yaml

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
# limitations under the License.
1414

1515
#{{ $mcp := oas3_to_mcp $.Values.spec_file }}
16+
#{{ $base_path := $.Values.base_path }}
17+
#{{- if not $base_path }}
18+
# {{ $base_path = print "/mcp" (include "get_basepath" (index $.Values.spec.servers 0 "url") | trimSuffix "/") }}
19+
#{{- end }}
1620
APIProxy:
1721
.name: mcp-{{ slug_make ($.Values.spec.info.title) }}
1822
DisplayName: {{ $.Values.spec.info.title }}
@@ -270,19 +274,89 @@ Policies:
270274
}
271275
}
272276
IgnoreUnresolvedVariables: true
277+
- AssignMessage:
278+
.continueOnError: false
279+
.enabled: true
280+
.name: AM-SaveReq
281+
DisplayName: AM-SaveReq
282+
Properties: { }
283+
Copy:
284+
.source: request
285+
Headers: { }
286+
QueryParams: { }
287+
FormParams: { }
288+
Payload: true
289+
Verb: true
290+
Path: true
291+
IgnoreUnresolvedVariables: true
292+
AssignTo:
293+
.createNew: true
294+
.transport: http
295+
.type: request
296+
-Data: original_request
273297
- CORS:
274298
.continueOnError: false
275299
.enabled: true
276300
.name: CORS-Allow
277301
DisplayName: CORS-Allow
278302
AllowOrigins: '{request.header.origin:*}'
279-
AllowMethods: POST
303+
AllowMethods: POST,GET,HEAD
280304
AllowHeaders: '*'
281305
ExposeHeaders: '*'
282306
MaxAge: 3628800
283307
AllowCredentials: true
284308
GeneratePreflightResponse: true
285309
IgnoreUnresolvedVariables: true
310+
#{{- if $mcp.auth_server }}
311+
- RaiseFault:
312+
.continueOnError: false
313+
.enabled: true
314+
.name: RF-Set401
315+
DisplayName: RF-Set401
316+
Properties: { }
317+
FaultResponse:
318+
- Set:
319+
Headers:
320+
- Header:
321+
.name: WWW-Authenticate
322+
-Data: Bearer resource_metadata="{client.scheme}://{original_request.header.host}/.well-known/oauth-protected-resource{proxy.basepath}"
323+
- AssignVariable:
324+
Name: error_status
325+
Value: 401
326+
- AssignVariable:
327+
Name: error_body
328+
Template: |-
329+
{
330+
"jsonrpc": "2.0",
331+
"id": {mcp.id},
332+
"error": {
333+
"code": -32001,
334+
"message": "Unauthorized"
335+
}
336+
}
337+
IgnoreUnresolvedVariables: true
338+
- AssignMessage:
339+
.continueOnError: false
340+
.enabled: true
341+
.name: AM-SetMetadataRes
342+
DisplayName: AM-SetMetadataRes
343+
Properties: { }
344+
Set:
345+
Payload:
346+
.contentType: application/json
347+
-Data: |-
348+
{
349+
"resource": "{client.scheme}://{original_request.header.host}{{ $base_path }}",
350+
"resource_name": "{{ $.Values.spec.info.title }}",
351+
"authorization_servers": [ "{{ $mcp.auth_server.issuer_url }}" ],
352+
{{- if eq $mcp.auth_server.type "oauth2" }}
353+
"scopes_supported": {{ $mcp.auth_server.scopes | toJson }},
354+
{{- end }}
355+
"bearer_methods_supported": [ "header" ]
356+
}
357+
StatusCode: 200
358+
ReasonPhrase: OK
359+
#{{- end }}
286360
ProxyEndpoints:
287361
- ProxyEndpoint:
288362
.name: default
@@ -302,11 +376,18 @@ ProxyEndpoints:
302376
.name: PreFlow
303377
Request:
304378
-Data:
379+
- Step:
380+
Name: AM-SaveReq
305381
- Step:
306382
Name: CORS-Allow
307383
- Step:
308384
Name: JS-ProcessReq
309385
Condition: request.verb = "POST" and request.header.Content-Type = "application/json"
386+
#{{- if $mcp.auth_server }}
387+
- Step:
388+
Name: RF-Set401
389+
Condition: request.header.authorization = null || NOT (request.header.authorization =| "Bearer ")
390+
#{{- end }}
310391
- PostFlow:
311392
.name: PostFlow
312393
- Flows:
@@ -370,18 +451,48 @@ ProxyEndpoints:
370451
Step:
371452
Name: RF-Method404
372453
- HTTPProxyConnection:
373-
BasePath: >-
374-
{{ if $.Values.base_path -}}
375-
{{ $.Values.base_path | trimSuffix "/" }}
376-
{{- else -}}
377-
/mcp{{ include "get_basepath" (index $.Values.spec.servers 0 "url") | trimSuffix "/" }}
378-
{{- end }}
454+
BasePath: {{ $base_path }}
379455
- RouteRule:
380456
.name: tool-call
381457
TargetEndpoint: tool-call
382458
Condition: mcp.method = "tools/call"
383459
- RouteRule:
384460
.name: no-op
461+
#{{- if $mcp.auth_server }}
462+
- ProxyEndpoint:
463+
.name: protected-resource-metadata
464+
-Data:
465+
- DefaultFaultRule:
466+
.name: default-fault
467+
Step:
468+
Name: AM-SetGenericError
469+
- FaultRules:
470+
-Data:
471+
- FaultRule:
472+
.name: custom-error
473+
Step:
474+
Name: AM-SetCustomError
475+
Condition: error_body != null
476+
- PreFlow:
477+
.name: PreFlow
478+
Request:
479+
-Data:
480+
- Step:
481+
Name: AM-SaveReq
482+
- Step:
483+
Name: CORS-Allow
484+
Response:
485+
Step:
486+
Name: AM-SetMetadataRes
487+
- PostFlow:
488+
.name: PostFlow
489+
- Flows: []
490+
- HTTPProxyConnection:
491+
BasePath: /.well-known/oauth-protected-resource{{ $base_path }}
492+
- RouteRule:
493+
.name: no-op
494+
#{{- end }}
495+
385496
TargetEndpoints:
386497
- TargetEndpoint:
387498
.name: tool-call

pkg/utils/mcp/mcp.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ type ToolTarget struct {
4646
type ValuesFile struct {
4747
ToolsList []*Tool `yaml:"tools_list"`
4848
ToolsTargets map[string]*ToolTarget `yaml:"tools_targets"`
49+
AuthServer AuthorizationServer `yaml:"auth_server"`
4950
}
5051

5152
func OAS3ToMCPValues(file string) (mcpValuesMap map[string]any, err error) {
@@ -266,9 +267,16 @@ func OAS3ToMCPValues(file string) (mcpValuesMap map[string]any, err error) {
266267
}
267268
}
268269

270+
var authServer AuthorizationServer
271+
if authServer, err = SelectAuthorizationServer(oas3Node); err != nil {
272+
return nil, err
273+
}
274+
269275
valuesFile := &ValuesFile{
270276
ToolsList: mcpToolsList,
271-
ToolsTargets: mcpToolsTargets}
277+
ToolsTargets: mcpToolsTargets,
278+
AuthServer: authServer,
279+
}
272280

273281
var valuesFileContent []byte
274282
if valuesFileContent, err = yaml.Marshal(valuesFile); err != nil {

0 commit comments

Comments
 (0)