pomerium/authorize/evaluator/mcp.go
Denis Mishin f5c5326c72
mcp: respond with jsonrpc error when MCP request is denied (#5694)
## Summary

Individual MCP method calls may be denied (i.e. via `mcp_tool`
criterion) and Pomerium has to respond with MCP protocol error, which is
JSON-RPC error message, rather then with HTTP level error which seems to
break some MCP clients.


## Related issues

Fix
https://linear.app/pomerium/issue/ENG-2521/pomerium-does-not-return-an-mcp-error-when-a-tool-call-is-unauthorized

## User Explanation

<!-- How would you explain this change to the user? If this
change doesn't create any user-facing changes, you can leave
this blank. If filled out, add the `docs` label -->

## Checklist

- [x] reference any related issues
- [x] updated unit tests
- [x] add appropriate label (`enhancement`, `bug`, `breaking`,
`dependencies`, `ci`)
- [x] ready for review
2025-07-08 09:07:26 -06:00

71 lines
1.9 KiB
Go

package evaluator
import (
"encoding/json"
"errors"
"fmt"
"mime"
"net/http"
envoy_service_auth_v3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
"github.com/pomerium/pomerium/internal/jsonrpc"
)
// RequestMCP is the MCP field in the request.
type RequestMCP struct {
ID jsonrpc.ID `json:"id"`
Method string `json:"method,omitempty"`
ToolCall *RequestMCPToolCall `json:"tool_call,omitempty"`
}
// RequestMCPToolCall represents a tool call within an MCP request.
type RequestMCPToolCall struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments"`
}
// RequestMCPFromCheckRequest populates a RequestMCP from an Envoy CheckRequest proto for MCP routes.
func RequestMCPFromCheckRequest(
in *envoy_service_auth_v3.CheckRequest,
) (RequestMCP, error) {
var mcpReq RequestMCP
ht := in.GetAttributes().GetRequest().GetHttp()
if ht.Method != http.MethodPost {
return mcpReq, nil
}
body := ht.GetBody()
if body == "" {
return mcpReq, errors.New("MCP request body is empty")
}
contentType := ht.GetHeaders()["content-type"]
mimeType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return mcpReq, fmt.Errorf("failed to parse content-type %q: %w", contentType, err)
}
if mimeType != "application/json" {
return mcpReq, fmt.Errorf("unsupported content-type %q, expected application/json", mimeType)
}
jsonRPCReq, err := jsonrpc.ParseRequest([]byte(body))
if err != nil {
return mcpReq, fmt.Errorf("failed to parse MCP request: %w", err)
}
mcpReq.ID = jsonRPCReq.ID
mcpReq.Method = jsonRPCReq.Method
if jsonRPCReq.Method == "tools/call" {
var toolCall RequestMCPToolCall
err := json.Unmarshal(jsonRPCReq.Params, &toolCall)
if err != nil {
return mcpReq, fmt.Errorf("failed to unmarshal MCP tool call parameters: %w", err)
}
mcpReq.ToolCall = &toolCall
}
return mcpReq, nil
}