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
18 changes: 18 additions & 0 deletions docs/public/schemas/mcp-gateway-config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
110 changes: 110 additions & 0 deletions docs/src/content/docs/reference/mcp-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions pkg/types/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,22 @@ 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
Entrypoint string `json:"entrypoint,omitempty" yaml:"entrypoint,omitempty"` // Optional entrypoint override for container
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"`
}
43 changes: 41 additions & 2 deletions pkg/workflow/mcp_config_custom.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
}
Comment on lines 100 to 106
Copy link

Copilot AI Mar 31, 2026

Choose a reason for hiding this comment

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

There are existing unit tests covering getMCPConfig behavior, but no tests exercise the new auth handling (parsing, type restrictions, and rendering order). Adding focused tests would help prevent regressions: (1) auth is accepted and round-trips for HTTP servers, (2) auth on stdio is rejected, (3) missing/unsupported auth.type and unknown auth fields produce clear errors, and (4) JSON rendering includes auth in the intended property order after headers.

This issue also appears in the following locations of the same file:

  • line 574
  • line 707

Copilot uses AI. Check for mistakes.
}
default:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 7 additions & 0 deletions pkg/workflow/mcp_config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
25 changes: 25 additions & 0 deletions pkg/workflow/mcp_config_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"]
Expand Down
Loading
Loading