@@ -3,9 +3,12 @@ package app
3
3
import (
4
4
"context"
5
5
"fmt"
6
+ "strings"
6
7
7
8
"github.com/spf13/cobra"
8
9
10
+ "github.com/stacklok/toolhive/pkg/auth"
11
+ "github.com/stacklok/toolhive/pkg/authz"
9
12
cfg "github.com/stacklok/toolhive/pkg/config"
10
13
"github.com/stacklok/toolhive/pkg/container"
11
14
"github.com/stacklok/toolhive/pkg/container/runtime"
@@ -16,6 +19,7 @@ import (
16
19
"github.com/stacklok/toolhive/pkg/registry"
17
20
"github.com/stacklok/toolhive/pkg/runner"
18
21
"github.com/stacklok/toolhive/pkg/runner/retriever"
22
+ "github.com/stacklok/toolhive/pkg/telemetry"
19
23
"github.com/stacklok/toolhive/pkg/transport"
20
24
"github.com/stacklok/toolhive/pkg/transport/types"
21
25
)
@@ -191,14 +195,53 @@ func BuildRunnerConfig(
191
195
debugMode bool ,
192
196
cmd * cobra.Command ,
193
197
) (* runner.RunConfig , error ) {
194
- // Validate the host flag
198
+ // Validate and setup basic configuration
195
199
validatedHost , err := ValidateAndNormaliseHostFlag (runFlags .Host )
196
200
if err != nil {
197
201
return nil , fmt .Errorf ("invalid host: %s" , runFlags .Host )
198
202
}
199
203
200
- // Get OIDC flags
204
+ // Setup OIDC configuration
205
+ oidcConfig , err := setupOIDCConfiguration (cmd , runFlags )
206
+ if err != nil {
207
+ return nil , err
208
+ }
209
+
210
+ // Setup telemetry configuration
211
+ telemetryConfig := setupTelemetryConfiguration (cmd , runFlags )
212
+
213
+ // Setup runtime and validation
214
+ rt , envVarValidator , err := setupRuntimeAndValidation (ctx )
215
+ if err != nil {
216
+ return nil , err
217
+ }
218
+
219
+ // Handle image retrieval
220
+ imageURL , imageMetadata , err := handleImageRetrieval (ctx , serverOrImage , runFlags )
221
+ if err != nil {
222
+ return nil , err
223
+ }
224
+
225
+ // Validate and setup proxy mode
226
+ if err := validateAndSetupProxyMode (runFlags ); err != nil {
227
+ return nil , err
228
+ }
229
+
230
+ // Parse environment variables
231
+ envVars , err := environment .ParseEnvironmentVariables (runFlags .Env )
232
+ if err != nil {
233
+ return nil , fmt .Errorf ("failed to parse environment variables: %v" , err )
234
+ }
235
+
236
+ // Build the runner config
237
+ return buildRunnerConfig (ctx , runFlags , cmdArgs , debugMode , validatedHost , rt , imageURL , imageMetadata ,
238
+ envVars , envVarValidator , oidcConfig , telemetryConfig )
239
+ }
240
+
241
+ // setupOIDCConfiguration sets up OIDC configuration and validates URLs
242
+ func setupOIDCConfiguration (cmd * cobra.Command , runFlags * RunFlags ) (* auth.TokenValidatorConfig , error ) {
201
243
oidcIssuer , oidcAudience , oidcJwksURL , oidcIntrospectionURL , oidcClientID , oidcClientSecret := getOidcFromFlags (cmd )
244
+
202
245
if oidcJwksURL != "" {
203
246
if err := networking .ValidateEndpointURL (oidcJwksURL ); err != nil {
204
247
return nil , fmt .Errorf ("invalid %s: %w" , oidcJwksURL , err )
@@ -210,61 +253,85 @@ func BuildRunnerConfig(
210
253
}
211
254
}
212
255
213
- // Get OTEL flag values with config fallbacks
256
+ return createOIDCConfig (oidcIssuer , oidcAudience , oidcJwksURL , oidcIntrospectionURL ,
257
+ oidcClientID , oidcClientSecret , runFlags .ResourceURL ), nil
258
+ }
259
+
260
+ // setupTelemetryConfiguration sets up telemetry configuration with config fallbacks
261
+ func setupTelemetryConfiguration (cmd * cobra.Command , runFlags * RunFlags ) * telemetry.Config {
214
262
config := cfg .GetConfig ()
215
263
finalOtelEndpoint , finalOtelSamplingRate , finalOtelEnvironmentVariables := getTelemetryFromFlags (cmd , config ,
216
264
runFlags .OtelEndpoint , runFlags .OtelSamplingRate , runFlags .OtelEnvironmentVariables )
217
265
218
- // Create container runtime
266
+ return createTelemetryConfig (finalOtelEndpoint , runFlags .OtelEnablePrometheusMetricsPath ,
267
+ runFlags .OtelServiceName , finalOtelSamplingRate , runFlags .OtelHeaders , runFlags .OtelInsecure ,
268
+ finalOtelEnvironmentVariables )
269
+ }
270
+
271
+ // setupRuntimeAndValidation creates container runtime and selects environment variable validator
272
+ func setupRuntimeAndValidation (ctx context.Context ) (runtime.Deployer , runner.EnvVarValidator , error ) {
219
273
rt , err := container .NewFactory ().Create (ctx )
220
274
if err != nil {
221
- return nil , fmt .Errorf ("failed to create container runtime: %v" , err )
275
+ return nil , nil , fmt .Errorf ("failed to create container runtime: %v" , err )
222
276
}
223
277
224
- // Select an envVars var validation strategy depending on how the CLI is run:
225
- // If we have called the CLI directly, we use the CLIEnvVarValidator.
226
- // If we are running in detached mode, or the CLI is wrapped by the K8s operator,
227
- // we use the DetachedEnvVarValidator.
228
278
var envVarValidator runner.EnvVarValidator
229
279
if process .IsDetached () || runtime .IsKubernetesRuntime () {
230
280
envVarValidator = & runner.DetachedEnvVarValidator {}
231
281
} else {
232
282
envVarValidator = & runner.CLIEnvVarValidator {}
233
283
}
234
284
235
- // Image retrieval
285
+ return rt , envVarValidator , nil
286
+ }
287
+
288
+ // handleImageRetrieval retrieves and processes the MCP server image
289
+ func handleImageRetrieval (
290
+ ctx context.Context , serverOrImage string , runFlags * RunFlags ,
291
+ ) (string , * registry.ImageMetadata , error ) {
236
292
var imageMetadata * registry.ImageMetadata
237
293
imageURL := serverOrImage
238
- // Only pull image if we are not running in Kubernetes mode.
239
- // This split will go away if we implement a separate command or binary
240
- // for running MCP servers in Kubernetes.
294
+
241
295
if ! runtime .IsKubernetesRuntime () {
242
- // Take the MCP server we were supplied and either fetch the image, or
243
- // build it from a protocol scheme. If the server URI refers to an image
244
- // in our trusted registry, we will also fetch the image metadata.
296
+ var err error
245
297
imageURL , imageMetadata , err = retriever .GetMCPServer (ctx , serverOrImage , runFlags .CACertPath , runFlags .VerifyImage )
246
298
if err != nil {
247
- return nil , fmt .Errorf ("failed to find or create the MCP server %s: %v" , serverOrImage , err )
299
+ return "" , nil , fmt .Errorf ("failed to find or create the MCP server %s: %v" , serverOrImage , err )
248
300
}
249
301
}
250
302
251
- // Validate proxy mode early
303
+ return imageURL , imageMetadata , nil
304
+ }
305
+
306
+ // validateAndSetupProxyMode validates and sets default proxy mode if needed
307
+ func validateAndSetupProxyMode (runFlags * RunFlags ) error {
252
308
if ! types .IsValidProxyMode (runFlags .ProxyMode ) {
253
309
if runFlags .ProxyMode == "" {
254
310
runFlags .ProxyMode = types .ProxyModeSSE .String () // default to SSE for backward compatibility
255
311
} else {
256
- return nil , fmt .Errorf ("invalid value for --proxy-mode: %s" , runFlags .ProxyMode )
312
+ return fmt .Errorf ("invalid value for --proxy-mode: %s" , runFlags .ProxyMode )
257
313
}
258
314
}
315
+ return nil
316
+ }
259
317
260
- // Parse the environment variables from a list of strings to a map.
261
- envVars , err := environment .ParseEnvironmentVariables (runFlags .Env )
262
- if err != nil {
263
- return nil , fmt .Errorf ("failed to parse environment variables: %v" , err )
264
- }
265
-
266
- // Initialize a new RunConfig with values from command-line flags
267
- return runner .NewRunConfigBuilder ().
318
+ // buildRunnerConfig creates the final RunnerConfig using the builder pattern
319
+ func buildRunnerConfig (
320
+ ctx context.Context ,
321
+ runFlags * RunFlags ,
322
+ cmdArgs []string ,
323
+ debugMode bool ,
324
+ validatedHost string ,
325
+ rt runtime.Deployer ,
326
+ imageURL string ,
327
+ imageMetadata * registry.ImageMetadata ,
328
+ envVars map [string ]string ,
329
+ envVarValidator runner.EnvVarValidator ,
330
+ oidcConfig * auth.TokenValidatorConfig ,
331
+ telemetryConfig * telemetry.Config ,
332
+ ) (* runner.RunConfig , error ) {
333
+ // Create a builder for the RunConfig
334
+ builder := runner .NewRunConfigBuilder ().
268
335
WithRuntime (rt ).
269
336
WithCmdArgs (cmdArgs ).
270
337
WithName (runFlags .Name ).
@@ -284,16 +351,59 @@ func BuildRunnerConfig(
284
351
WithAuditEnabled (runFlags .EnableAudit , runFlags .AuditConfig ).
285
352
WithLabels (runFlags .Labels ).
286
353
WithGroup (runFlags .Group ).
287
- WithOIDCConfig (oidcIssuer , oidcAudience , oidcJwksURL , oidcIntrospectionURL , oidcClientID , oidcClientSecret ,
288
- runFlags .ThvCABundle , runFlags .JWKSAuthTokenFile , runFlags .ResourceURL , runFlags .JWKSAllowPrivateIP ).
289
- WithTelemetryConfig (finalOtelEndpoint , runFlags .OtelEnablePrometheusMetricsPath , runFlags .OtelServiceName ,
290
- finalOtelSamplingRate , runFlags .OtelHeaders , runFlags .OtelInsecure , finalOtelEnvironmentVariables ).
291
- WithToolsFilter (runFlags .ToolsFilter ).
292
354
WithIgnoreConfig (& ignore.Config {
293
355
LoadGlobal : runFlags .IgnoreGlobally ,
294
356
PrintOverlays : runFlags .PrintOverlays ,
295
- }).
296
- Build (ctx , imageMetadata , envVars , envVarValidator )
357
+ })
358
+
359
+ // Configure middleware from flags
360
+ builder = builder .WithMiddlewareFromFlags (
361
+ oidcConfig ,
362
+ runFlags .ToolsFilter ,
363
+ telemetryConfig ,
364
+ runFlags .AuthzConfig ,
365
+ runFlags .EnableAudit ,
366
+ runFlags .AuditConfig ,
367
+ runFlags .Name ,
368
+ runFlags .Transport ,
369
+ )
370
+
371
+ // Load authz config if path is provided
372
+ if runFlags .AuthzConfig != "" {
373
+ if authzConfigData , err := authz .LoadConfig (runFlags .AuthzConfig ); err == nil {
374
+ builder = builder .WithAuthzConfig (authzConfigData )
375
+ }
376
+ // Note: Path is already set via WithAuthzConfigPath above
377
+ }
378
+
379
+ // Get OIDC and telemetry values for legacy configuration
380
+ oidcIssuer , oidcAudience , oidcJwksURL , oidcIntrospectionURL , oidcClientID , oidcClientSecret := extractOIDCValues (oidcConfig )
381
+ finalOtelEndpoint , finalOtelSamplingRate , finalOtelEnvironmentVariables := extractTelemetryValues (telemetryConfig )
382
+
383
+ // Set additional configurations that are still needed in old format for other parts of the system
384
+ builder = builder .WithOIDCConfig (oidcIssuer , oidcAudience , oidcJwksURL , oidcIntrospectionURL , oidcClientID , oidcClientSecret ,
385
+ runFlags .ThvCABundle , runFlags .JWKSAuthTokenFile , runFlags .ResourceURL , runFlags .JWKSAllowPrivateIP ).
386
+ WithTelemetryConfig (finalOtelEndpoint , runFlags .OtelEnablePrometheusMetricsPath , runFlags .OtelServiceName ,
387
+ finalOtelSamplingRate , runFlags .OtelHeaders , runFlags .OtelInsecure , finalOtelEnvironmentVariables ).
388
+ WithToolsFilter (runFlags .ToolsFilter )
389
+
390
+ return builder .Build (ctx , imageMetadata , envVars , envVarValidator )
391
+ }
392
+
393
+ // extractOIDCValues extracts OIDC values from the OIDC config for legacy configuration
394
+ func extractOIDCValues (config * auth.TokenValidatorConfig ) (string , string , string , string , string , string ) {
395
+ if config == nil {
396
+ return "" , "" , "" , "" , "" , ""
397
+ }
398
+ return config .Issuer , config .Audience , config .JWKSURL , config .IntrospectionURL , config .ClientID , config .ClientSecret
399
+ }
400
+
401
+ // extractTelemetryValues extracts telemetry values from the telemetry config for legacy configuration
402
+ func extractTelemetryValues (config * telemetry.Config ) (string , float64 , []string ) {
403
+ if config == nil {
404
+ return "" , 0.0 , nil
405
+ }
406
+ return config .Endpoint , config .SamplingRate , config .EnvironmentVariables
297
407
}
298
408
299
409
// getOidcFromFlags extracts OIDC configuration from command flags
@@ -329,3 +439,69 @@ func getTelemetryFromFlags(cmd *cobra.Command, config *cfg.Config, otelEndpoint
329
439
330
440
return finalOtelEndpoint , finalOtelSamplingRate , finalOtelEnvironmentVariables
331
441
}
442
+
443
+ // createOIDCConfig creates an OIDC configuration if any OIDC parameters are provided
444
+ func createOIDCConfig (oidcIssuer , oidcAudience , oidcJwksURL , oidcIntrospectionURL ,
445
+ oidcClientID , oidcClientSecret , resourceURL string ) * auth.TokenValidatorConfig {
446
+ if oidcIssuer != "" || oidcAudience != "" || oidcJwksURL != "" || oidcIntrospectionURL != "" ||
447
+ oidcClientID != "" || oidcClientSecret != "" || resourceURL != "" {
448
+ return & auth.TokenValidatorConfig {
449
+ Issuer : oidcIssuer ,
450
+ Audience : oidcAudience ,
451
+ JWKSURL : oidcJwksURL ,
452
+ IntrospectionURL : oidcIntrospectionURL ,
453
+ ClientID : oidcClientID ,
454
+ ClientSecret : oidcClientSecret ,
455
+ ResourceURL : resourceURL ,
456
+ }
457
+ }
458
+ return nil
459
+ }
460
+
461
+ // createTelemetryConfig creates a telemetry configuration if any telemetry parameters are provided
462
+ func createTelemetryConfig (otelEndpoint string , otelEnablePrometheusMetricsPath bool ,
463
+ otelServiceName string , otelSamplingRate float64 , otelHeaders []string ,
464
+ otelInsecure bool , otelEnvironmentVariables []string ) * telemetry.Config {
465
+ if otelEndpoint == "" && ! otelEnablePrometheusMetricsPath {
466
+ return nil
467
+ }
468
+
469
+ // Parse headers from key=value format
470
+ headers := make (map [string ]string )
471
+ for _ , header := range otelHeaders {
472
+ parts := strings .SplitN (header , "=" , 2 )
473
+ if len (parts ) == 2 {
474
+ headers [parts [0 ]] = parts [1 ]
475
+ }
476
+ }
477
+
478
+ // Use provided service name or default
479
+ serviceName := otelServiceName
480
+ if serviceName == "" {
481
+ serviceName = telemetry .DefaultConfig ().ServiceName
482
+ }
483
+
484
+ // Process environment variables - split comma-separated values
485
+ var processedEnvVars []string
486
+ for _ , envVarEntry := range otelEnvironmentVariables {
487
+ // Split by comma and trim whitespace
488
+ envVars := strings .Split (envVarEntry , "," )
489
+ for _ , envVar := range envVars {
490
+ trimmed := strings .TrimSpace (envVar )
491
+ if trimmed != "" {
492
+ processedEnvVars = append (processedEnvVars , trimmed )
493
+ }
494
+ }
495
+ }
496
+
497
+ return & telemetry.Config {
498
+ Endpoint : otelEndpoint ,
499
+ ServiceName : serviceName ,
500
+ ServiceVersion : telemetry .DefaultConfig ().ServiceVersion ,
501
+ SamplingRate : otelSamplingRate ,
502
+ Headers : headers ,
503
+ Insecure : otelInsecure ,
504
+ EnablePrometheusMetricsPath : otelEnablePrometheusMetricsPath ,
505
+ EnvironmentVariables : processedEnvVars ,
506
+ }
507
+ }
0 commit comments