Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions api/v1alpha1/olsconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,10 @@ type MCPServerStreamableHTTPTransport struct {
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="SSE Read Timeout in seconds"
SSEReadTimeout int `json:"sseReadTimeout,omitempty"`
// Headers to send to the MCP server
// the map contains the header name and the name of the secret with the content of the header. This secret
// should contain a header path in the data containing a header value.
// A special case is usage of the kubernetes token in the header. to specify this use
// a string "kubernetes" instead of the secret name
// +operator-sdk:csv:customresourcedefinitions:type=spec,displayName="Headers"
Headers map[string]string `json:"headers,omitempty"`
// Enable Server Sent Events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,12 @@ spec:
- description: Enable Server Sent Events
displayName: Enable Server Sent Events
path: mcpServers[0].streamableHTTP.enableSSE
- description: Headers to send to the MCP server
- description: |-
Headers to send to the MCP server
the map contains the header name and the name of the secret with the content of the header. This secret
should contain a header path in the data containing a header value.
A special case is usage of the kubernetes token in the header. to specify this use
a string "kubernetes" instead of the secret name
displayName: Headers
path: mcpServers[0].streamableHTTP.headers
- description: SSE Read Timeout, default is 10 seconds
Expand Down
7 changes: 6 additions & 1 deletion bundle/manifests/ols.openshift.io_olsconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,12 @@ spec:
headers:
additionalProperties:
type: string
description: Headers to send to the MCP server
description: |-
Headers to send to the MCP server
the map contains the header name and the name of the secret with the content of the header. This secret
should contain a header path in the data containing a header value.
A special case is usage of the kubernetes token in the header. to specify this use
a string "kubernetes" instead of the secret name
type: object
sseReadTimeout:
default: 10
Expand Down
7 changes: 6 additions & 1 deletion config/crd/bases/ols.openshift.io_olsconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,12 @@ spec:
headers:
additionalProperties:
type: string
description: Headers to send to the MCP server
description: |-
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bundle generation needed here

Headers to send to the MCP server
the map contains the header name and the name of the secret with the content of the header. This secret
should contain a header path in the data containing a header value.
A special case is usage of the kubernetes token in the header. to specify this use
a string "kubernetes" instead of the secret name
type: object
sseReadTimeout:
default: 10
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ spec:
- description: Enable Server Sent Events
displayName: Enable Server Sent Events
path: mcpServers[0].streamableHTTP.enableSSE
- description: Headers to send to the MCP server
- description: |-
Headers to send to the MCP server
the map contains the header name and the name of the secret with the content of the header. This secret
should contain a header path in the data containing a header value.
A special case is usage of the kubernetes token in the header. to specify this use
a string "kubernetes" instead of the secret name
displayName: Headers
path: mcpServers[0].streamableHTTP.headers
- description: SSE Read Timeout, default is 10 seconds
Expand Down
8 changes: 8 additions & 0 deletions internal/controller/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,12 @@ ssl_ca_file = '/etc/certs/cm-olspostgresca/service-ca.crt'
OpenShiftMCPServerTimeout = 60
// MCP server SSE read timeout, sec
OpenShiftMCPServerHTTPReadTimeout = 30
// Authorization header for OpenShift MCP server
K8S_AUTH_HEADER = "kubernetes-authorization"
// Constant, defining usage of kubernetes token
KUBERNETES_PLACEHOLDER = "kubernetes"
// MCPHeadersMountRoot is the directory hosting MCP headers in the container
MCPHeadersMountRoot = "/etc/mcp/headers"
// Header Secret Data Path
MCPSECRETDATAPATH = "header"
)
69 changes: 56 additions & 13 deletions internal/controller/ols_app_server_assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,13 +344,17 @@ func (r *OLSConfigReconciler) generateOLSConfigMap(ctx context.Context, cr *olsv
URL: fmt.Sprintf(OpenShiftMCPServerURL, OpenShiftMCPServerPort),
Timeout: OpenShiftMCPServerTimeout,
SSEReadTimeout: OpenShiftMCPServerHTTPReadTimeout,
Headers: map[string]string{K8S_AUTH_HEADER: KUBERNETES_PLACEHOLDER},
},
},
}
}

if cr.Spec.FeatureGates != nil && slices.Contains(cr.Spec.FeatureGates, FeatureGateMCPServer) {
mcpServers := generateMCPServerConfigs(cr)
mcpServers, err := r.generateMCPServerConfigs(ctx, cr)
if err != nil {
return nil, err
}
if appSrvConfigFile.MCPServers == nil {
appSrvConfigFile.MCPServers = mcpServers
} else {
Expand Down Expand Up @@ -730,47 +734,86 @@ const (
StreamableHTTPField
)

func generateMCPServerConfigs(cr *olsv1alpha1.OLSConfig) []MCPServerConfig {
func (r *OLSConfigReconciler) generateMCPServerConfigs(ctx context.Context, cr *olsv1alpha1.OLSConfig) ([]MCPServerConfig, error) {
if cr.Spec.MCPServers == nil {
return nil
return nil, nil
}

servers := []MCPServerConfig{}
var overall_error error
overall_error = nil
for _, server := range cr.Spec.MCPServers {
// check all the secrets
sse, err := r.generateMCPStreamableHTTPTransportConfig(ctx, &server, SSEField)
if err != nil {
overall_error = err
continue
}
streamableHTTP, err := r.generateMCPStreamableHTTPTransportConfig(ctx, &server, StreamableHTTPField)
if err != nil {
overall_error = err
continue
}
servers = append(servers, MCPServerConfig{
Name: server.Name,
Transport: getMCPTransport(&server),
SSE: generateMCPStreamableHTTPTransportConfig(&server, SSEField),
StreamableHTTP: generateMCPStreamableHTTPTransportConfig(&server, StreamableHTTPField),
SSE: sse,
StreamableHTTP: streamableHTTP,
})
}
return servers
return servers, overall_error
}

func generateMCPStreamableHTTPTransportConfig(server *olsv1alpha1.MCPServer, field int) *StreamableHTTPTransportConfig {
func (r *OLSConfigReconciler) generateMCPStreamableHTTPTransportConfig(ctx context.Context, server *olsv1alpha1.MCPServer, field int) (*StreamableHTTPTransportConfig, error) {
if server == nil || server.StreamableHTTP == nil {
return nil
return nil, nil
}

switch field {
case SSEField:
if !server.StreamableHTTP.EnableSSE {
return nil
return nil, nil
}
case StreamableHTTPField:
if server.StreamableHTTP.EnableSSE {
return nil
return nil, nil
}
default:
return nil
return nil, nil
}

// convert headers to paths
headers := make(map[string]string, len(server.StreamableHTTP.Headers))
for k, v := range server.StreamableHTTP.Headers {
if v == KUBERNETES_PLACEHOLDER {
headers[k] = v
} else {
secret := &corev1.Secret{}
err := r.Get(ctx, client.ObjectKey{Name: v, Namespace: r.Options.Namespace}, secret)
if err != nil {
if apierrors.IsNotFound(err) {
r.logger.Error(err, fmt.Sprint("Header secret ", v, " for MCP server ", server.Name, " is not found"))
return nil, fmt.Errorf("MCP %s header secret %s is not found", server.Name, v)
}
r.logger.Error(err, fmt.Sprint("Failed to get header", v, " for MCP server ", server.Name))
return nil, fmt.Errorf("failed to get secret %s for MCP provider %s: %w", v, server.Name, err)
}
// make sure the secret has header path
if _, ok := secret.Data[MCPSECRETDATAPATH]; !ok {
r.logger.Error(err, fmt.Sprint("Header", v, " for MCP server ", server.Name, " does not contain 'header' path"))
return nil, fmt.Errorf("header %s for MCP server %s is missing key 'header'", v, server.Name)
}
// update header
headers[k] = path.Join(MCPHeadersMountRoot, v, MCPSECRETDATAPATH)
}
}

return &StreamableHTTPTransportConfig{
URL: server.StreamableHTTP.URL,
Timeout: server.StreamableHTTP.Timeout,
SSEReadTimeout: server.StreamableHTTP.SSEReadTimeout,
Headers: server.StreamableHTTP.Headers,
}
Headers: headers,
}, nil
}

func getMCPTransport(server *olsv1alpha1.MCPServer) MCPTransport {
Expand Down
Loading