From 9cdad57717433f21044236cb581c877e06d49b3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:36:37 +0000 Subject: [PATCH 1/3] Initial plan From 7d48dc17c24a4ac780185d154d45a6d14aa44106 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 00:58:36 +0000 Subject: [PATCH 2/3] Add MCP Gateway upstream OIDC authentication support to schema, docs, and compiler - Add MCPAuthConfig struct and Auth field to BaseMCPServerConfig in pkg/types/mcp.go - Implement GetAny method on MapToolConfig in pkg/workflow/mcp_config_types.go - Update mcpServerConfigToMap to include auth field in pkg/workflow/tools_types.go - Add auth field parsing and JSON rendering in pkg/workflow/mcp_config_custom.go - Add auth property to httpServerConfig in both schema files - Add auth row to Section 4.1.2 and new Section 7.6 in mcp-gateway.md Closes #23566 Agent-Logs-Url: https://github.com/github/gh-aw/sessions/fe03ae40-ff9a-43ab-b882-da184a4adbcd Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- .../schemas/mcp-gateway-config.schema.json | 18 +++ .../src/content/docs/reference/mcp-gateway.md | 110 ++++++++++++++++++ pkg/types/mcp.go | 12 ++ pkg/workflow/mcp_config_custom.go | 43 ++++++- pkg/workflow/mcp_config_types.go | 7 ++ .../schemas/mcp-gateway-config.schema.json | 18 +++ pkg/workflow/tools_types.go | 3 + 7 files changed, 209 insertions(+), 2 deletions(-) diff --git a/docs/public/schemas/mcp-gateway-config.schema.json b/docs/public/schemas/mcp-gateway-config.schema.json index 0c7119e6877..7d37896ef1a 100644 --- a/docs/public/schemas/mcp-gateway-config.schema.json +++ b/docs/public/schemas/mcp-gateway-config.schema.json @@ -165,6 +165,24 @@ "type": "object", "description": "Guard policies for access control at the MCP gateway level. The structure of guard policies is server-specific. For GitHub MCP server, see the GitHub guard policy schema. For other servers (Jira, WorkIQ), different policy schemas will apply.", "additionalProperties": true + }, + "auth": { + "type": "object", + "description": "Upstream authentication configuration for the HTTP MCP server. When configured, the gateway dynamically acquires tokens and injects them as Authorization headers on every outgoing request to this server. Currently only GitHub Actions OIDC is supported.", + "properties": { + "type": { + "type": "string", + "enum": ["github-oidc"], + "description": "Authentication type. Currently only 'github-oidc' is supported, which acquires short-lived JWTs from the GitHub Actions OIDC endpoint." + }, + "audience": { + "type": "string", + "description": "The intended audience for the OIDC token (the 'aud' claim). If omitted, defaults to the server's url field.", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false } }, "required": ["type", "url"], diff --git a/docs/src/content/docs/reference/mcp-gateway.md b/docs/src/content/docs/reference/mcp-gateway.md index af0e8d361f0..bc1581f824f 100644 --- a/docs/src/content/docs/reference/mcp-gateway.md +++ b/docs/src/content/docs/reference/mcp-gateway.md @@ -228,6 +228,7 @@ Each server configuration MUST support: | `registry` | string | No | URI to the installation location when MCP is installed from a registry. This is an informational field used for documentation and tooling discovery. Applies to both stdio and HTTP servers. Example: `"https://api.mcp.github.com/v0/servers/microsoft/markitdown"` | | `tools` | array[string] | No | Tool filter for the MCP server. Use `["*"]` to allow all tools (default), or specify a list of tool names to allow. This field is passed through to agent configurations and applies to both stdio and http servers. | | `headers` | object | No | HTTP headers to include in requests (HTTP servers only). Commonly used for authentication to external HTTP servers. Values may contain variable expressions. | +| `auth` | object | No | Upstream authentication configuration for HTTP servers. See [Section 7.6](#76-upstream-authentication-oidc). | *Required for stdio servers (containerized execution) **Required for HTTP servers @@ -994,6 +995,115 @@ Workflow authors set this via the `sandbox.mcp.trusted-bots` frontmatter field; --- +### 7.6 Upstream Authentication (OIDC) + +HTTP MCP servers MAY configure upstream authentication using the `auth` field. When present, the gateway dynamically acquires tokens and injects them as `Authorization: Bearer` headers on every outgoing request to the server. + +#### 7.6.1 GitHub Actions OIDC + +When `auth.type` is `"github-oidc"`, the gateway acquires short-lived JWTs from the GitHub Actions OIDC endpoint. This requires the workflow to have `permissions: { id-token: write }`. + +**Configuration**: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | Yes | Must be `"github-oidc"` | +| `audience` | string | No | The intended audience (`aud` claim) for the OIDC token. Defaults to the server `url` if omitted. | + +**Environment Variables** (set automatically by GitHub Actions): + +| Variable | Description | +|----------|-------------| +| `ACTIONS_ID_TOKEN_REQUEST_URL` | OIDC token endpoint URL | +| `ACTIONS_ID_TOKEN_REQUEST_TOKEN` | Bearer token for authenticating to the OIDC endpoint | + +**Behavior**: + +1. On startup, the gateway checks for `ACTIONS_ID_TOKEN_REQUEST_URL`. If set, an OIDC provider is initialized. +2. If a server has `auth.type: "github-oidc"` but the OIDC env vars are missing, the gateway MUST log an error at startup and MUST return an error when the server is first accessed. +3. Tokens are cached per audience and refreshed proactively before expiry (60-second margin). +4. The OIDC `Authorization: Bearer` header overwrites any static `Authorization` header from the `headers` field. Other static headers pass through. +5. The gateway does NOT verify JWT signatures — it acts as a token acquirer/forwarder. The downstream MCP server is the relying party and MUST validate the token. + +**Example** (JSON stdin format): + +```json +{ + "mcpServers": { + "my-mcp-server": { + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": { + "type": "github-oidc", + "audience": "https://my-server.example.com" + } + } + } +} +``` + +**Example with audience defaulting to URL**: + +```json +{ + "mcpServers": { + "my-mcp-server": { + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": { + "type": "github-oidc" + } + } + } +} +``` + +In this case, the audience defaults to `"https://my-server.example.com/mcp"`. + +**Frontmatter Example** (workflow author): + +```yaml +tools: + mcp-servers: + my-mcp-server: + type: http + url: "https://my-server.example.com/mcp" + auth: + type: github-oidc + audience: "https://my-server.example.com" +``` + +#### 7.6.2 Interaction with Static Headers + +When both `headers` and `auth` are configured: + +- Static headers from `headers` are applied first +- The OIDC token overwrites the `Authorization` header +- All other static headers (e.g., `X-Custom-Header`) pass through unchanged + +This allows combining OIDC auth with non-auth headers: + +```json +{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "headers": { + "X-Custom-Header": "custom-value" + }, + "auth": { + "type": "github-oidc" + } +} +``` + +#### 7.6.3 Validation Rules + +- `auth` is only valid on HTTP servers (`type: "http"`). Stdio servers with `auth` MUST be rejected with a validation error. +- `auth.type` is required when `auth` is present. Empty type MUST be rejected. +- Unsupported `auth.type` values MUST be rejected with a descriptive error. + +--- + ## 8. Health Monitoring ### 8.1 Health Endpoints diff --git a/pkg/types/mcp.go b/pkg/types/mcp.go index 9524a7dd218..d3d0c971ad7 100644 --- a/pkg/types/mcp.go +++ b/pkg/types/mcp.go @@ -16,6 +16,7 @@ type BaseMCPServerConfig struct { // HTTP-specific fields URL string `json:"url,omitempty" yaml:"url,omitempty"` // URL for HTTP mode MCP servers Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"` // HTTP headers for HTTP mode + Auth *MCPAuthConfig `json:"auth,omitempty" yaml:"auth,omitempty"` // Upstream authentication config (HTTP mode only) // Container-specific fields Container string `json:"container,omitempty" yaml:"container,omitempty"` // Container image for the MCP server @@ -23,3 +24,14 @@ type BaseMCPServerConfig struct { EntrypointArgs []string `json:"entrypointArgs,omitempty" yaml:"entrypointArgs,omitempty"` // Arguments passed to container entrypoint Mounts []string `json:"mounts,omitempty" yaml:"mounts,omitempty"` // Volume mounts for container (format: "source:dest:mode") } + +// MCPAuthConfig represents upstream authentication configuration for an HTTP MCP server. +// When configured, the gateway dynamically acquires tokens and injects them as Authorization +// headers on every outgoing request. Currently only GitHub Actions OIDC is supported. +type MCPAuthConfig struct { + // Type is the authentication type. Currently only "github-oidc" is supported. + Type string `json:"type" yaml:"type"` + // Audience is the intended audience (aud claim) for the OIDC token. + // If omitted, defaults to the server's url field. + Audience string `json:"audience,omitempty" yaml:"audience,omitempty"` +} diff --git a/pkg/workflow/mcp_config_custom.go b/pkg/workflow/mcp_config_custom.go index e8ce9d208bf..eb6c0e33a13 100644 --- a/pkg/workflow/mcp_config_custom.go +++ b/pkg/workflow/mcp_config_custom.go @@ -100,9 +100,9 @@ func renderSharedMCPConfig(yaml *strings.Builder, toolName string, toolConfig ma // JSON format - include tools field for MCP gateway tool filtering (all engines) // For HTTP MCP with secrets in headers, env passthrough is needed if len(headerSecrets) > 0 { - propertyOrder = []string{"type", "url", "headers", "tools", "env"} + propertyOrder = []string{"type", "url", "headers", "auth", "tools", "env"} } else { - propertyOrder = []string{"type", "url", "headers", "tools"} + propertyOrder = []string{"type", "url", "headers", "auth", "tools"} } } default: @@ -162,6 +162,10 @@ func renderSharedMCPConfig(yaml *strings.Builder, toolName string, toolConfig ma if len(mcpConfig.Headers) > 0 { existingProperties = append(existingProperties, prop) } + case "auth": + if mcpConfig.Auth != nil { + existingProperties = append(existingProperties, prop) + } case "http_headers": if len(mcpConfig.Headers) > 0 { existingProperties = append(existingProperties, prop) @@ -468,6 +472,24 @@ func renderSharedMCPConfig(yaml *strings.Builder, toolName string, toolConfig ma fmt.Fprintf(yaml, "%s \"%s\": \"%s\"%s\n", renderer.IndentLevel, headerKey, headerValue, headerComma) } fmt.Fprintf(yaml, "%s}%s\n", renderer.IndentLevel, comma) + case "auth": + // Auth field - upstream OIDC authentication config (HTTP servers only, JSON format only) + // Guard against nil auth (defensive check, existingProperties should have filtered this out) + if mcpConfig.Auth == nil { + continue + } + comma := "," + if isLast { + comma = "" + } + fmt.Fprintf(yaml, "%s\"auth\": {\n", renderer.IndentLevel) + if mcpConfig.Auth.Audience != "" { + fmt.Fprintf(yaml, "%s \"type\": \"%s\",\n", renderer.IndentLevel, mcpConfig.Auth.Type) + fmt.Fprintf(yaml, "%s \"audience\": \"%s\"\n", renderer.IndentLevel, mcpConfig.Auth.Audience) + } else { + fmt.Fprintf(yaml, "%s \"type\": \"%s\"\n", renderer.IndentLevel, mcpConfig.Auth.Type) + } + fmt.Fprintf(yaml, "%s}%s\n", renderer.IndentLevel, comma) case "proxy-args": if renderer.Format == "toml" { fmt.Fprintf(yaml, "%sproxy_args = [\n", renderer.IndentLevel) @@ -564,6 +586,7 @@ func getMCPConfig(toolConfig map[string]any, toolName string) (*parser.MCPServer "proxy-args": true, "url": true, "headers": true, + "auth": true, "registry": true, "allowed": true, "toolsets": true, // Added for MCPServerConfig struct @@ -681,6 +704,22 @@ func getMCPConfig(toolConfig map[string]any, toolName string) (*parser.MCPServer if headers, hasHeaders := config.GetStringMap("headers"); hasHeaders { result.Headers = headers } + if authVal, hasAuth := config.GetAny("auth"); hasAuth { + if authMap, ok := authVal.(map[string]any); ok { + authConfig := &types.MCPAuthConfig{} + if authType, ok := authMap["type"].(string); ok { + authConfig.Type = authType + } + if audience, ok := authMap["audience"].(string); ok { + authConfig.Audience = audience + } + if authConfig.Type != "" { + result.Auth = authConfig + } + } else if authCfg, ok := authVal.(*types.MCPAuthConfig); ok { + result.Auth = authCfg + } + } default: mcpCustomLog.Printf("Unsupported MCP type '%s' for tool '%s'", result.Type, toolName) return nil, fmt.Errorf( diff --git a/pkg/workflow/mcp_config_types.go b/pkg/workflow/mcp_config_types.go index dfcc1112614..9a89a9f821a 100644 --- a/pkg/workflow/mcp_config_types.go +++ b/pkg/workflow/mcp_config_types.go @@ -109,3 +109,10 @@ func (m MapToolConfig) GetStringMap(key string) (map[string]string, bool) { } return nil, false } + +func (m MapToolConfig) GetAny(key string) (any, bool) { + if value, exists := m[key]; exists { + return value, true + } + return nil, false +} diff --git a/pkg/workflow/schemas/mcp-gateway-config.schema.json b/pkg/workflow/schemas/mcp-gateway-config.schema.json index 99749c51bba..d5063297b6c 100644 --- a/pkg/workflow/schemas/mcp-gateway-config.schema.json +++ b/pkg/workflow/schemas/mcp-gateway-config.schema.json @@ -147,6 +147,24 @@ "type": "object", "description": "Guard policies for access control at the MCP gateway level. The structure of guard policies is server-specific. For GitHub MCP server, see the GitHub guard policy schema. For other servers (Jira, WorkIQ), different policy schemas will apply.", "additionalProperties": true + }, + "auth": { + "type": "object", + "description": "Upstream authentication configuration for the HTTP MCP server. When configured, the gateway dynamically acquires tokens and injects them as Authorization headers on every outgoing request to this server. Currently only GitHub Actions OIDC is supported.", + "properties": { + "type": { + "type": "string", + "enum": ["github-oidc"], + "description": "Authentication type. Currently only 'github-oidc' is supported, which acquires short-lived JWTs from the GitHub Actions OIDC endpoint." + }, + "audience": { + "type": "string", + "description": "The intended audience for the OIDC token (the 'aud' claim). If omitted, defaults to the server's url field.", + "format": "uri" + } + }, + "required": ["type"], + "additionalProperties": false } }, "required": ["type", "url"], diff --git a/pkg/workflow/tools_types.go b/pkg/workflow/tools_types.go index 2419c50ae40..980ee6dcdb4 100644 --- a/pkg/workflow/tools_types.go +++ b/pkg/workflow/tools_types.go @@ -137,6 +137,9 @@ func mcpServerConfigToMap(config MCPServerConfig) map[string]any { if len(config.Headers) > 0 { result["headers"] = config.Headers } + if config.Auth != nil { + result["auth"] = config.Auth + } // Add container-specific fields if config.Container != "" { From f28f999e17b4cc8f509f604454e4f7d4c19ad118 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 01:51:01 +0000 Subject: [PATCH 3/3] Add tests and validation for MCP auth field - Add auth field validation in mcp_config_validation.go: - Add 'auth' to knownToolFields (prevents spurious unknown-property errors) - HTTP servers: validate auth.type is present, non-empty, and 'github-oidc' - Stdio servers: reject auth with a clear error message - Add TestGetMCPConfigWithAuth: round-trip parsing of auth for HTTP servers - Add auth cases to TestValidateMCPConfigs: valid, stdio-rejection, missing type, unsupported type, empty type - Add auth cases to TestMCPServerConfigToMap: set and nil auth - Add TestRenderSharedMCPConfig_WithAuth: JSON rendering with/without auth, property order Agent-Logs-Url: https://github.com/github/gh-aw/sessions/aab4c252-327f-4964-b814-cd5bb33643f1 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- pkg/workflow/mcp_config_validation.go | 25 +++++ pkg/workflow/mcp_http_headers_test.go | 115 +++++++++++++++++++ pkg/workflow/mcp_json_test.go | 152 ++++++++++++++++++++++++++ pkg/workflow/tools_types_test.go | 48 ++++++++ 4 files changed, 340 insertions(+) diff --git a/pkg/workflow/mcp_config_validation.go b/pkg/workflow/mcp_config_validation.go index 48cd8ec7400..b7e0508c0f2 100644 --- a/pkg/workflow/mcp_config_validation.go +++ b/pkg/workflow/mcp_config_validation.go @@ -129,6 +129,7 @@ func getRawMCPConfig(toolConfig map[string]any) (map[string]any, error) { "container": true, "env": true, "headers": true, + "auth": true, // upstream OIDC authentication (HTTP servers only) "version": true, "args": true, "entrypoint": true, @@ -233,9 +234,33 @@ func validateMCPRequirements(toolName string, mcpConfig map[string]any, toolConf return fmt.Errorf("tool '%s' mcp configuration with type 'http' cannot use 'mounts' field. Volume mounts are only supported for stdio (containerized) MCP servers.\n\nExample:\ntools:\n %s:\n type: http\n url: \"https://api.example.com/mcp\"\n\nSee: %s", toolName, toolName, constants.DocsToolsURL) } + // Validate auth if present: must have a valid type field + if authRaw, hasAuth := toolConfig["auth"]; hasAuth { + authMap, ok := authRaw.(map[string]any) + if !ok { + return fmt.Errorf("tool '%s' mcp configuration 'auth' must be an object.\n\nExample:\ntools:\n %s:\n type: http\n url: \"https://api.example.com/mcp\"\n auth:\n type: github-oidc\n\nSee: %s", toolName, toolName, constants.DocsToolsURL) + } + authType, hasAuthType := authMap["type"] + if !hasAuthType { + return fmt.Errorf("tool '%s' mcp configuration 'auth.type' is required.\n\nExample:\ntools:\n %s:\n type: http\n url: \"https://api.example.com/mcp\"\n auth:\n type: github-oidc\n\nSee: %s", toolName, toolName, constants.DocsToolsURL) + } + authTypeStr, ok := authType.(string) + if !ok || authTypeStr == "" { + return fmt.Errorf("tool '%s' mcp configuration 'auth.type' must be a non-empty string. Currently only 'github-oidc' is supported.\n\nExample:\ntools:\n %s:\n type: http\n url: \"https://api.example.com/mcp\"\n auth:\n type: github-oidc\n\nSee: %s", toolName, toolName, constants.DocsToolsURL) + } + if authTypeStr != "github-oidc" { + return fmt.Errorf("tool '%s' mcp configuration 'auth.type' value %q is not supported. Currently only 'github-oidc' is supported.\n\nExample:\ntools:\n %s:\n type: http\n url: \"https://api.example.com/mcp\"\n auth:\n type: github-oidc\n\nSee: %s", toolName, authTypeStr, toolName, constants.DocsToolsURL) + } + } + return validateStringProperty(toolName, "url", url, hasURL) case "stdio": + // stdio type does not support auth (auth is only valid for HTTP servers) + if _, hasAuth := toolConfig["auth"]; hasAuth { + return fmt.Errorf("tool '%s' mcp configuration 'auth' is only supported for HTTP servers (type: 'http'). Stdio servers do not support upstream authentication.\n\nIf you need upstream auth, use an HTTP MCP server:\ntools:\n %s:\n type: http\n url: \"https://api.example.com/mcp\"\n auth:\n type: github-oidc\n\nSee: %s", toolName, toolName, constants.DocsToolsURL) + } + // stdio type requires either 'command' or 'container' property (but not both) command, hasCommand := mcpConfig["command"] container, hasContainer := mcpConfig["container"] diff --git a/pkg/workflow/mcp_http_headers_test.go b/pkg/workflow/mcp_http_headers_test.go index 2aa8937f902..10c9598f349 100644 --- a/pkg/workflow/mcp_http_headers_test.go +++ b/pkg/workflow/mcp_http_headers_test.go @@ -339,3 +339,118 @@ func TestRenderSharedMCPConfig_PropertyOrder(t *testing.T) { t.Errorf("Properties are not in expected order (type, url, headers, tools, env):\n%s", result) } } + +func TestRenderSharedMCPConfig_WithAuth(t *testing.T) { + t.Run("renders auth after headers with type and audience", func(t *testing.T) { + toolConfig := map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "headers": map[string]any{ + "X-Custom-Header": "custom-value", + }, + "auth": map[string]any{ + "type": "github-oidc", + "audience": "https://my-server.example.com", + }, + } + + renderer := MCPConfigRenderer{ + IndentLevel: " ", + Format: "json", + RequiresCopilotFields: true, + } + + var output strings.Builder + err := renderSharedMCPConfig(&output, "my-server", toolConfig, renderer) + if err != nil { + t.Fatalf("renderSharedMCPConfig failed: %v", err) + } + + result := output.String() + + // Auth block is rendered + if !strings.Contains(result, `"auth": {`) { + t.Errorf("Expected auth block not found in output:\n%s", result) + } + if !strings.Contains(result, `"type": "github-oidc"`) { + t.Errorf("Expected auth.type not found in output:\n%s", result) + } + if !strings.Contains(result, `"audience": "https://my-server.example.com"`) { + t.Errorf("Expected auth.audience not found in output:\n%s", result) + } + + // Property order: type < url < headers < auth < tools + typeIdx := strings.Index(result, `"type": "http"`) + urlIdx := strings.Index(result, `"url":`) + headersIdx := strings.Index(result, `"headers":`) + authIdx := strings.Index(result, `"auth":`) + toolsIdx := strings.Index(result, `"tools":`) + + if typeIdx == -1 || urlIdx == -1 || headersIdx == -1 || authIdx == -1 || toolsIdx == -1 { + t.Fatalf("Missing required properties in output:\n%s", result) + } + + if typeIdx >= urlIdx || urlIdx >= headersIdx || headersIdx >= authIdx || authIdx >= toolsIdx { + t.Errorf("Properties not in expected order (type, url, headers, auth, tools):\n%s", result) + } + }) + + t.Run("renders auth without audience (type only)", func(t *testing.T) { + toolConfig := map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": map[string]any{ + "type": "github-oidc", + }, + } + + renderer := MCPConfigRenderer{ + IndentLevel: " ", + Format: "json", + RequiresCopilotFields: true, + } + + var output strings.Builder + err := renderSharedMCPConfig(&output, "my-server", toolConfig, renderer) + if err != nil { + t.Fatalf("renderSharedMCPConfig failed: %v", err) + } + + result := output.String() + + if !strings.Contains(result, `"auth": {`) { + t.Errorf("Expected auth block not found:\n%s", result) + } + if !strings.Contains(result, `"type": "github-oidc"`) { + t.Errorf("Expected auth.type not found:\n%s", result) + } + if strings.Contains(result, `"audience"`) { + t.Errorf("Unexpected audience field in output:\n%s", result) + } + }) + + t.Run("no auth block when auth is absent", func(t *testing.T) { + toolConfig := map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + } + + renderer := MCPConfigRenderer{ + IndentLevel: " ", + Format: "json", + RequiresCopilotFields: true, + } + + var output strings.Builder + err := renderSharedMCPConfig(&output, "my-server", toolConfig, renderer) + if err != nil { + t.Fatalf("renderSharedMCPConfig failed: %v", err) + } + + result := output.String() + + if strings.Contains(result, `"auth"`) { + t.Errorf("Unexpected auth block in output:\n%s", result) + } + }) +} diff --git a/pkg/workflow/mcp_json_test.go b/pkg/workflow/mcp_json_test.go index ca52af126a7..1458ce40d8a 100644 --- a/pkg/workflow/mcp_json_test.go +++ b/pkg/workflow/mcp_json_test.go @@ -112,6 +112,75 @@ func TestGetMCPConfig(t *testing.T) { } } +func TestGetMCPConfigWithAuth(t *testing.T) { + t.Run("http server with auth type and audience round-trips", func(t *testing.T) { + toolConfig := map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": map[string]any{ + "type": "github-oidc", + "audience": "https://my-server.example.com", + }, + } + + result, err := getMCPConfig(toolConfig, "my-server") + if err != nil { + t.Fatalf("getMCPConfig() unexpected error: %v", err) + } + + if result.Auth == nil { + t.Fatal("expected Auth to be set, got nil") + } + if result.Auth.Type != "github-oidc" { + t.Errorf("expected Auth.Type = 'github-oidc', got %q", result.Auth.Type) + } + if result.Auth.Audience != "https://my-server.example.com" { + t.Errorf("expected Auth.Audience = 'https://my-server.example.com', got %q", result.Auth.Audience) + } + }) + + t.Run("http server with auth type only (no audience)", func(t *testing.T) { + toolConfig := map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": map[string]any{ + "type": "github-oidc", + }, + } + + result, err := getMCPConfig(toolConfig, "my-server") + if err != nil { + t.Fatalf("getMCPConfig() unexpected error: %v", err) + } + + if result.Auth == nil { + t.Fatal("expected Auth to be set, got nil") + } + if result.Auth.Type != "github-oidc" { + t.Errorf("expected Auth.Type = 'github-oidc', got %q", result.Auth.Type) + } + if result.Auth.Audience != "" { + t.Errorf("expected Auth.Audience to be empty, got %q", result.Auth.Audience) + } + }) + + t.Run("http server without auth has nil Auth", func(t *testing.T) { + toolConfig := map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + } + + result, err := getMCPConfig(toolConfig, "my-server") + if err != nil { + t.Fatalf("getMCPConfig() unexpected error: %v", err) + } + + if result.Auth != nil { + t.Errorf("expected Auth to be nil, got %+v", result.Auth) + } + }) +} + func TestHasMCPConfig(t *testing.T) { tests := []struct { name string @@ -429,6 +498,89 @@ func TestValidateMCPConfigs(t *testing.T) { wantErr: true, errMsg: "unknown property 'network'", }, + { + name: "http server with valid auth config is accepted", + tools: map[string]any{ + "oidc-server": map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": map[string]any{ + "type": "github-oidc", + "audience": "https://my-server.example.com", + }, + }, + }, + wantErr: false, + }, + { + name: "http server with auth type only (no audience) is accepted", + tools: map[string]any{ + "oidc-server": map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": map[string]any{ + "type": "github-oidc", + }, + }, + }, + wantErr: false, + }, + { + name: "stdio server with auth is rejected", + tools: map[string]any{ + "stdio-with-auth": map[string]any{ + "type": "stdio", + "container": "mcp/server:latest", + "auth": map[string]any{ + "type": "github-oidc", + }, + }, + }, + wantErr: true, + errMsg: "'auth' is only supported for HTTP servers", + }, + { + name: "auth without type field is rejected", + tools: map[string]any{ + "bad-auth": map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": map[string]any{ + "audience": "https://my-server.example.com", + }, + }, + }, + wantErr: true, + errMsg: "'auth.type' is required", + }, + { + name: "auth with unsupported type is rejected", + tools: map[string]any{ + "bad-auth-type": map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": map[string]any{ + "type": "bearer-token", + }, + }, + }, + wantErr: true, + errMsg: "not supported", + }, + { + name: "auth with empty type string is rejected", + tools: map[string]any{ + "empty-auth-type": map[string]any{ + "type": "http", + "url": "https://my-server.example.com/mcp", + "auth": map[string]any{ + "type": "", + }, + }, + }, + wantErr: true, + errMsg: "must be a non-empty string", + }, } for _, tt := range tests { diff --git a/pkg/workflow/tools_types_test.go b/pkg/workflow/tools_types_test.go index d2f67fc207f..8f94e9c4a7e 100644 --- a/pkg/workflow/tools_types_test.go +++ b/pkg/workflow/tools_types_test.go @@ -710,4 +710,52 @@ func TestMCPServerConfigToMap(t *testing.T) { t.Errorf("expected customField, got %v", result["customField"]) } }) + + t.Run("includes auth field when set", func(t *testing.T) { + config := MCPServerConfig{ + BaseMCPServerConfig: types.BaseMCPServerConfig{ + Type: "http", + URL: "https://my-server.example.com/mcp", + Auth: &types.MCPAuthConfig{ + Type: "github-oidc", + Audience: "https://my-server.example.com", + }, + }, + } + + result := mcpServerConfigToMap(config) + + authVal, hasAuth := result["auth"] + if !hasAuth { + t.Fatal("expected 'auth' key in result map, but it was absent") + } + + authConfig, ok := authVal.(*types.MCPAuthConfig) + if !ok { + t.Fatalf("expected auth to be *types.MCPAuthConfig, got %T", authVal) + } + + if authConfig.Type != "github-oidc" { + t.Errorf("expected auth.Type = 'github-oidc', got %q", authConfig.Type) + } + if authConfig.Audience != "https://my-server.example.com" { + t.Errorf("expected auth.Audience = 'https://my-server.example.com', got %q", authConfig.Audience) + } + }) + + t.Run("omits auth field when nil", func(t *testing.T) { + config := MCPServerConfig{ + BaseMCPServerConfig: types.BaseMCPServerConfig{ + Type: "http", + URL: "https://my-server.example.com/mcp", + Auth: nil, + }, + } + + result := mcpServerConfigToMap(config) + + if _, hasAuth := result["auth"]; hasAuth { + t.Error("expected 'auth' key to be absent when Auth is nil") + } + }) }