Skip to content

Commit 061fceb

Browse files
committed
feat: enhance MCPServer environment variable configuration with EnvVarCfg type
Signed-off-by: Andrew Hemming <andrew@footprintit.net>
1 parent 89d1509 commit 061fceb

6 files changed

Lines changed: 200 additions & 20 deletions

File tree

api/v1alpha1/mcpserver_types.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ type MCPServerDeployment struct {
215215

216216
// Env defines the environment variables to set in the container.
217217
// +optional
218-
Env map[string]string `json:"env,omitempty"`
218+
Env map[string]EnvVarCfg `json:"env,omitempty"`
219219

220220
// SecretRefs defines the list of Kubernetes secrets to reference.
221221
// These secrets will be mounted as volumes to the MCP server container.
@@ -247,6 +247,18 @@ type MCPServerDeployment struct {
247247
ServiceAccount *ServiceAccountConfig `json:"serviceAccount,omitempty"`
248248
}
249249

250+
// EnvVarCfg allows specifying either a literal value or a reference to a source for the environment variable.
251+
type EnvVarCfg struct {
252+
// Value contains the value for the environment variable.
253+
// Defaults to "" if not set.
254+
// +optional
255+
Value string `json:"value,omitempty"`
256+
257+
// ValueFrom specifies a source the value of this EnvVar to come from.
258+
// +optional
259+
ValueFrom *corev1.EnvVarSource `json:"valueFrom,omitempty"`
260+
}
261+
250262
// InitContainerConfig defines the configuration for the init container.
251263
type InitContainerConfig struct {
252264
// Image defines the full image reference for the init container.

api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 22 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/kagent.dev_mcpservers.yaml

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,109 @@ spec:
8484
type: array
8585
env:
8686
additionalProperties:
87-
type: string
87+
description: EnvVarCfg allows specifying either a literal value
88+
or a reference to a source for the environment variable.
89+
properties:
90+
value:
91+
description: |-
92+
Value contains the value for the environment variable.
93+
Defaults to "" if not set.
94+
type: string
95+
valueFrom:
96+
description: ValueFrom specifies a source the value of this
97+
EnvVar to come from.
98+
properties:
99+
configMapKeyRef:
100+
description: Selects a key of a ConfigMap.
101+
properties:
102+
key:
103+
description: The key to select.
104+
type: string
105+
name:
106+
default: ""
107+
description: |-
108+
Name of the referent.
109+
This field is effectively required, but due to backwards compatibility is
110+
allowed to be empty. Instances of this type with an empty value here are
111+
almost certainly wrong.
112+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
113+
type: string
114+
optional:
115+
description: Specify whether the ConfigMap or its
116+
key must be defined
117+
type: boolean
118+
required:
119+
- key
120+
type: object
121+
x-kubernetes-map-type: atomic
122+
fieldRef:
123+
description: |-
124+
Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['<KEY>']`, `metadata.annotations['<KEY>']`,
125+
spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs.
126+
properties:
127+
apiVersion:
128+
description: Version of the schema the FieldPath
129+
is written in terms of, defaults to "v1".
130+
type: string
131+
fieldPath:
132+
description: Path of the field to select in the
133+
specified API version.
134+
type: string
135+
required:
136+
- fieldPath
137+
type: object
138+
x-kubernetes-map-type: atomic
139+
resourceFieldRef:
140+
description: |-
141+
Selects a resource of the container: only resources limits and requests
142+
(limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported.
143+
properties:
144+
containerName:
145+
description: 'Container name: required for volumes,
146+
optional for env vars'
147+
type: string
148+
divisor:
149+
anyOf:
150+
- type: integer
151+
- type: string
152+
description: Specifies the output format of the
153+
exposed resources, defaults to "1"
154+
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
155+
x-kubernetes-int-or-string: true
156+
resource:
157+
description: 'Required: resource to select'
158+
type: string
159+
required:
160+
- resource
161+
type: object
162+
x-kubernetes-map-type: atomic
163+
secretKeyRef:
164+
description: Selects a key of a secret in the pod's
165+
namespace
166+
properties:
167+
key:
168+
description: The key of the secret to select from. Must
169+
be a valid secret key.
170+
type: string
171+
name:
172+
default: ""
173+
description: |-
174+
Name of the referent.
175+
This field is effectively required, but due to backwards compatibility is
176+
allowed to be empty. Instances of this type with an empty value here are
177+
almost certainly wrong.
178+
More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
179+
type: string
180+
optional:
181+
description: Specify whether the Secret or its key
182+
must be defined
183+
type: boolean
184+
required:
185+
- key
186+
type: object
187+
x-kubernetes-map-type: atomic
188+
type: object
189+
type: object
88190
description: Env defines the environment variables to set in the
89191
container.
90192
type: object

pkg/cli/internal/commands/deploy.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ func runPackageDeploy(_ *cobra.Command, _ []string) error {
217217
Port: uint16(port),
218218
Cmd: packageManager,
219219
Args: deployArgs,
220-
Env: envMap,
220+
Env: toEnvVarCfgMap(envMap),
221221
SecretRefs: secretRefs,
222222
},
223223
TransportType: getTransportType(),
@@ -468,7 +468,7 @@ func generateMCPServer(
468468
Port: uint16(port),
469469
Cmd: command,
470470
Args: args,
471-
Env: envVars,
471+
Env: toEnvVarCfgMap(envVars),
472472
SecretRefs: secretRefs,
473473
},
474474
TransportType: transportType,
@@ -571,6 +571,15 @@ func parseEnvVars(envVars []string) map[string]string {
571571
return result
572572
}
573573

574+
// toEnvVarCfgMap converts a map[string]string to a map[string]v1alpha1.EnvVarCfg
575+
func toEnvVarCfgMap(envMapString map[string]string) map[string]v1alpha1.EnvVarCfg {
576+
result := make(map[string]v1alpha1.EnvVarCfg)
577+
for k, v := range envMapString {
578+
result[k] = v1alpha1.EnvVarCfg{Value: v}
579+
}
580+
return result
581+
}
582+
574583
func applyToCluster(projectDir, yamlContent string, mcpServer *v1alpha1.MCPServer) error {
575584
fmt.Printf("🚀 Applying MCPServer to cluster...\n")
576585

pkg/controller/transportadapter/transportadapter_translator.go

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
102102
return nil, fmt.Errorf("image must be specified for MCPServer %s or the command must be 'uvx' or 'npx'", server.Name)
103103
}
104104

105+
// Convert EnvVarCfg to corev1.EnvVar for deployment
106+
envVars, err := convertEnvVars(server.Spec.Deployment.Env)
107+
if err != nil {
108+
return nil, err
109+
}
110+
105111
// Create environment variables from secrets for envFrom
106112
secretEnvFrom := t.createSecretEnvFrom(server.Spec.Deployment.SecretRefs)
107113

@@ -163,7 +169,7 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
163169
"-f",
164170
"/config/local.yaml",
165171
},
166-
Env: convertEnvVars(server.Spec.Deployment.Env),
172+
Env: envVars,
167173
EnvFrom: secretEnvFrom,
168174
VolumeMounts: append([]corev1.VolumeMount{
169175
{
@@ -209,7 +215,7 @@ func (t *transportAdapterTranslator) translateTransportAdapterDeployment(
209215
ImagePullPolicy: mainContainerPullPolicy,
210216
Command: cmd,
211217
Args: server.Spec.Deployment.Args,
212-
Env: convertEnvVars(server.Spec.Deployment.Env),
218+
Env: envVars,
213219
EnvFrom: secretEnvFrom,
214220
VolumeMounts: append([]corev1.VolumeMount{
215221
{
@@ -398,21 +404,50 @@ func (t *transportAdapterTranslator) createVolumeMounts(
398404
return volumeMounts
399405
}
400406

401-
func convertEnvVars(env map[string]string) []corev1.EnvVar {
407+
func convertEnvVars(env map[string]v1alpha1.EnvVarCfg) ([]corev1.EnvVar, error) {
402408
if env == nil {
403-
return nil
409+
return nil, nil
404410
}
405411
envVars := make([]corev1.EnvVar, 0, len(env))
406-
for key, value := range env {
407-
envVars = append(envVars, corev1.EnvVar{
408-
Name: key,
409-
Value: value,
410-
})
412+
for key, cfg := range env {
413+
if cfg.Value != "" && cfg.ValueFrom != nil {
414+
return nil, fmt.Errorf("environment variable %s cannot specify both value and valueFrom", key)
415+
}
416+
if cfg.Value != "" {
417+
envVars = append(envVars, corev1.EnvVar{
418+
Name: key,
419+
Value: cfg.Value,
420+
})
421+
} else if cfg.ValueFrom != nil {
422+
envVars = append(envVars, corev1.EnvVar{
423+
Name: key,
424+
ValueFrom: cfg.ValueFrom,
425+
})
426+
}
411427
}
412428
sort.Slice(envVars, func(i, j int) bool {
413429
return envVars[i].Name < envVars[j].Name
414430
})
415-
return envVars
431+
return envVars, nil
432+
}
433+
434+
// createStdioEnvConfig creates the environment variable configuration for the StdioTargetSpec.
435+
// It includes all EnvVarCfg entries, including those with ValueFrom.
436+
// Note: The underlying transport adapter (agentgateway) may not support ValueFrom in its config.
437+
func createStdioEnvConfig(env map[string]v1alpha1.EnvVarCfg) map[string]v1alpha1.EnvVarCfg {
438+
if env == nil {
439+
return nil
440+
}
441+
stdioEnvConfig := make(map[string]v1alpha1.EnvVarCfg)
442+
for key, cfg := range env {
443+
// Basic validation - this should ideally be handled by CRD validation
444+
if cfg.Value != "" && cfg.ValueFrom != nil {
445+
klog.Warningf("Environment variable %s in MCPServer.Spec.Deployment.Env specifies both value and valueFrom. Prioritizing valueFrom for Kubernetes Deployment, and including both for internal config. This may lead to unexpected behavior.", key)
446+
}
447+
// Pass through the EnvVarCfg as is, the generated struct expects this type
448+
stdioEnvConfig[key] = cfg
449+
}
450+
return stdioEnvConfig
416451
}
417452

418453
func (t *transportAdapterTranslator) translateTransportAdapterService(
@@ -506,7 +541,7 @@ func (t *transportAdapterTranslator) translateTransportAdapterConfig(server *v1a
506541
mcpTarget.Stdio = &StdioTargetSpec{
507542
Cmd: server.Spec.Deployment.Cmd,
508543
Args: server.Spec.Deployment.Args,
509-
Env: server.Spec.Deployment.Env,
544+
Env: createStdioEnvConfig(server.Spec.Deployment.Env),
510545
}
511546
case v1alpha1.TransportTypeHTTP:
512547
httpTransportConfig := server.Spec.HTTPTransport

pkg/controller/transportadapter/transportadapter_types.go

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)