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
11 changes: 8 additions & 3 deletions .github/aw/actions-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,21 @@
"version": "v4.36.0",
"sha": "7211b7c8077ea37d8641b6271f6a365a22a5fbfa"
},
"github/gh-aw-actions/setup@v0.75.4": {
"github/gh-aw-actions/setup@v0.76.1": {
"repo": "github/gh-aw-actions/setup",
"version": "v0.75.4",
"sha": "9f050961da586148d135e113d8bb025185cdf2b8"
"version": "v0.76.1",
"sha": "46d564922b082d0db93244972e8005ea6904ee5f"
},
"github/gh-aw/actions/setup-cli@v0.75.4": {
"repo": "github/gh-aw/actions/setup-cli",
"version": "v0.75.4",
"sha": "1a7f4119f6c4398ed2fc824f99276a55fb382e3f"
},
"github/gh-aw/actions/setup-cli@v0.76.1": {
"repo": "github/gh-aw/actions/setup-cli",
"version": "v0.76.1",
"sha": "58d1bedbb7200f59c2d224151339e38fd8687d05"
},
"github/gh-aw/actions/setup@v0.75.4": {
"repo": "github/gh-aw/actions/setup",
"version": "v0.75.4",
Expand Down
111 changes: 65 additions & 46 deletions .github/workflows/smoke-allowonly.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/smoke-allowonly.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ tools:
sandbox:
mcp:
container: "ghcr.io/github/gh-aw-mcpg"
version: "latest"
safe-outputs:
threat-detection:
enabled: false
Expand Down
77 changes: 48 additions & 29 deletions .github/workflows/smoke-copilot.lock.yml

Large diffs are not rendered by default.

75 changes: 47 additions & 28 deletions .github/workflows/smoke-long-session.lock.yml

Large diffs are not rendered by default.

101 changes: 60 additions & 41 deletions .github/workflows/smoke-otel-tracing.lock.yml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .github/workflows/smoke-otel-tracing.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ runtimes:
sandbox:
mcp:
container: "ghcr.io/github/gh-aw-mcpg"
version: "v0.3.15"
version: "latest"
steps:
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
Expand Down
1,299 changes: 0 additions & 1,299 deletions .github/workflows/smoke-proxy-github-script.invalid.yml

This file was deleted.

113 changes: 66 additions & 47 deletions .github/workflows/smoke-proxy-github-script.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/smoke-proxy-github-script.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ tools:
sandbox:
mcp:
container: "ghcr.io/github/gh-aw-mcpg"
version: "latest"
steps:
# ── Build the gateway container image from source ──────────────────
- name: Build MCP Gateway image
Expand Down
105 changes: 62 additions & 43 deletions .github/workflows/smoke-safeoutputs-discussions.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/smoke-safeoutputs-discussions.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ tools:
sandbox:
mcp:
container: "ghcr.io/github/gh-aw-mcpg"
version: "latest"
safe-outputs:
threat-detection:
enabled: false
Expand Down
105 changes: 62 additions & 43 deletions .github/workflows/smoke-safeoutputs-issues.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/smoke-safeoutputs-issues.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ tools:
sandbox:
mcp:
container: "ghcr.io/github/gh-aw-mcpg"
version: "latest"
safe-outputs:
threat-detection:
enabled: false
Expand Down
105 changes: 62 additions & 43 deletions .github/workflows/smoke-safeoutputs-labels.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/smoke-safeoutputs-labels.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ tools:
sandbox:
mcp:
container: "ghcr.io/github/gh-aw-mcpg"
version: "latest"
safe-outputs:
threat-detection:
enabled: false
Expand Down
109 changes: 64 additions & 45 deletions .github/workflows/smoke-safeoutputs-prs.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/smoke-safeoutputs-prs.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ tools:
sandbox:
mcp:
container: "ghcr.io/github/gh-aw-mcpg"
version: "latest"
safe-outputs:
threat-detection:
enabled: false
Expand Down
105 changes: 62 additions & 43 deletions .github/workflows/smoke-safeoutputs-reviews.lock.yml

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions .github/workflows/smoke-safeoutputs-reviews.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ tools:
sandbox:
mcp:
container: "ghcr.io/github/gh-aw-mcpg"
version: "latest"
safe-outputs:
threat-detection:
enabled: false
Expand Down
8 changes: 4 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Quick reference for AI agents working with MCP Gateway (Go-based MCP proxy serve
```toml
[gateway]
port = 3000
api_key = "your-api-key"
agent_id = "your-agent-id"
payload_dir = "/tmp/jq-payloads" # Optional: directory for large payload storage (must be absolute)

[servers.github]
Expand Down Expand Up @@ -392,7 +392,7 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
- `ACTIONS_ID_TOKEN_REQUEST_TOKEN` - GitHub Actions OIDC request token; required for `github-oidc` auth type
- `MCP_GATEWAY_PORT` - Used by environment validation (`--validate-env`) for container port-mapping checks (validated 1-65535); does not override the gateway listen address
- `MCP_GATEWAY_DOMAIN` - Used by environment validation (`--validate-env`) and containerized startup checks; to set config values use `gateway.domain` (or `"${MCP_GATEWAY_DOMAIN}"` in JSON stdin config)
- `MCP_GATEWAY_API_KEY` - Used by environment validation (`--validate-env`) and containerized startup checks; to enable auth set `gateway.apiKey` (commonly `"${MCP_GATEWAY_API_KEY}"` in JSON stdin config)
- `MCP_GATEWAY_AGENT_ID` - Used by environment validation (`--validate-env`) and containerized startup checks; to enable auth set `gateway.agentId` (commonly `"${MCP_GATEWAY_AGENT_ID}"` in JSON stdin config)
- `DEBUG` - Enable debug logging (e.g., `DEBUG=*`, `DEBUG=server:*,launcher:*`)
- `DEBUG_COLORS` - Control colored output (0 to disable, auto-disabled when piping)
- `MCP_GATEWAY_LOG_DIR` - Log file directory (sets default for `--log-dir` flag, default: `/tmp/gh-aw/mcp-logs`)
Expand Down Expand Up @@ -494,8 +494,8 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml

## Security Notes

- **Auth**: `Authorization: <apiKey>` header (plain API key per spec 7.1, NOT Bearer scheme)
- **Sessions**: Session ID extracted from Authorization header value
- **Auth**: `Authorization: <agentId>` header (plain value per spec 7.1, NOT Bearer scheme)
- **Sessions**: Session ID extracted from `X-Agent-ID` (preferred) or Authorization header value
- **Stdio servers**: Containerized execution only (no direct command support)
- **mTLS**: Mutual TLS can be enabled with `--tls-cert`, `--tls-key`, and `--tls-ca` flags (or corresponding env vars) to require client certificates for all connections
- **HMAC request signing**: Set `--hmac-secret` (or `MCP_GATEWAY_HMAC_SECRET`) to require HMAC-SHA256 signed requests; protects against replay attacks using `X-MCP-Timestamp`, `X-MCP-Nonce`, and `X-MCP-Signature` headers
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This gateway is used with [GitHub Agentic Workflows](https://github.com/github/g
```json
{
"gateway": {
"apiKey": "${MCP_GATEWAY_API_KEY}"
"agentId": "${MCP_GATEWAY_AGENT_ID}"
},
"mcpServers": {
"github": {
Expand All @@ -36,7 +36,7 @@ This gateway is used with [GitHub Agentic Workflows](https://github.com/github/g
docker run --rm -i \
-e MCP_GATEWAY_PORT=8000 \
-e MCP_GATEWAY_DOMAIN=localhost \
-e MCP_GATEWAY_API_KEY=your-secret-key \
-e MCP_GATEWAY_AGENT_ID=your-agent-id \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /path/to/logs:/tmp/gh-aw/mcp-logs \
-p 8000:8000 \
Expand Down Expand Up @@ -181,7 +181,8 @@ Key configuration fields (gateway-level under `[gateway]` in TOML / `"gateway"`

| Field | Description |
|-------|-------------|
| `api_key` / `apiKey` | API key for gateway authentication (MCP spec 7.1) |
| `agent_id` / `agentId` | Agent/session identifier used for routing and optional auth matching |
| `api_key` / `apiKey` | Deprecated alias for `agent_id` / `agentId` (accepted with warnings) |
| `port` | Listen port |
| `payload_dir` / `payloadDir` | Directory for large payload storage (must be absolute path) |
| `payload_size_threshold` / `payloadSizeThreshold` | Size threshold in bytes for payload storage (default: `524288`) |
Expand Down Expand Up @@ -215,7 +216,7 @@ For the full gateway field list (including rate limiting, tracing, keepalive, an

**Routing**: Routed mode (`/mcp/{serverID}`) exposes each backend at its own endpoint. Unified mode (`/mcp`) routes to all configured servers through a single endpoint.

**Security**: WASM-based DIFC guards enforce secrecy and integrity labels per request. Guards are loaded from `MCP_GATEWAY_WASM_GUARDS_DIR` and assigned per-server. Authentication uses plain API keys per MCP spec 7.1 (`Authorization: <api-key>`).
**Security**: WASM-based DIFC guards enforce secrecy and integrity labels per request. Guards are loaded from `MCP_GATEWAY_WASM_GUARDS_DIR` and assigned per-server. Authentication uses the configured agent identifier value per MCP spec 7.1 (`Authorization: <agent-id>`), and session routing can also use `X-Agent-ID`.

**Logging**: Per-server log files (`{serverID}.log`) and unified `mcp-gateway.log` use bracketed UTC ISO-8601 timestamps with milliseconds (`[YYYY-MM-DDTHH:mm:ss.SSSZ]`). Machine-readable `rpc-messages.jsonl` records include required `timestamp`, `event` (`snake_case`), and `_schema` fields; RPC events use `_schema: "rpc-message/v2"` with `event` values `rpc_request`/`rpc_response`, and DIFC filter records use `_schema: "difc-filtered/v2"` with `event: "difc_filtered"`. Markdown workflow previews (`gateway.md`) and the wazero cache (`<parent-of-log-dir>/wazero-cache`, a sibling of `--log-dir` by default) are also produced.

Expand Down
8 changes: 4 additions & 4 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
# This field is stored for metadata purposes only. Valid range: 1-65535
port = 3000

# API key for authentication (optional)
# When set, clients must provide this key in the Authorization header
# Format: Authorization: <api_key>
api_key = ""
# Agent ID for routing and optional authentication matching (optional)
# When set, clients must provide this agent ID in the Authorization header
# Format: Authorization: <agent_id>
agent_id = ""

# Domain name for the gateway (optional)
# Used for CORS and other domain-specific features
Expand Down
9 changes: 5 additions & 4 deletions docs/CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ TOML configuration requires `command = "docker"` for stdio-based MCP servers to
```toml
[gateway]
port = 3000
api_key = "your-api-key"
agent_id = "your-agent-id"

[servers.github]
command = "docker"
Expand Down Expand Up @@ -84,7 +84,7 @@ JSON configuration is the primary format for containerized deployments. Pass via
},
"gateway": {
"port": 8080,
"apiKey": "${MCP_GATEWAY_API_KEY}",
"agentId": "${MCP_GATEWAY_AGENT_ID}",
"domain": "localhost"
}
}
Expand Down Expand Up @@ -411,7 +411,7 @@ The `customSchemas` top-level field allows you to define custom server types bey
- **TOML format**:
- Uses `command` and `args` fields directly (e.g., `command = "docker"`)
- Variable expansion with `${VAR_NAME}` is only supported in `[gateway.opentelemetry]` and legacy `[gateway.tracing]` fields
- Server `env` values, `url`, `args`, `gateway.api_key`, and other non-tracing fields are not expanded
- Server `env` values, `url`, `args`, `gateway.agent_id`, and other non-tracing fields are not expanded
- For host environment passthrough to container `env`, use an empty string `""` value
- **Common rules** (both formats):
- Empty/"local" type automatically normalized to "stdio"
Expand All @@ -427,7 +427,8 @@ The `customSchemas` top-level field allows you to define custom server types bey
| Field | Description | Default |
|-------|-------------|---------|
| `port` | Validated and stored for metadata purposes only. The actual listen address is always set by the `--listen` CLI flag (default `127.0.0.1:3000`). | `3000` (informational only) |
| `apiKey` | API key for authentication | (disabled) |
| `agentId` | Agent/session identifier used for routing and optional auth matching | (disabled) |
| `apiKey` | Deprecated alias for `agentId` (accepted for backward compatibility) | (deprecated) |
| `domain` | Gateway domain (`"localhost"`, `"host.docker.internal"`, or `"${VAR}"`) | (unset) |
| `startupTimeout` | Seconds to wait for backend startup | `30` |
| `toolTimeout` | Maximum seconds for a single tool call, enforced as a context deadline on all backend requests (stdio and HTTP) | `60` |
Expand Down
6 changes: 4 additions & 2 deletions docs/ENVIRONMENT_VARIABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ When running in a container (`run_containerized.sh`), these variables **must** b
|----------|-------------|---------|
| `MCP_GATEWAY_PORT` | Port used by `run.sh`/`run_containerized.sh` to build the `--listen` address; also read by `awmg --validate-env` for port-mapping checks | `8000` |
| `MCP_GATEWAY_DOMAIN` | The domain name for the gateway | `localhost` |
| `MCP_GATEWAY_API_KEY` | API key checked by `run_containerized.sh` as a deployment gate; must be referenced in your JSON config via `"${MCP_GATEWAY_API_KEY}"` to enable authentication | `your-secret-key` |
| `MCP_GATEWAY_AGENT_ID` | Agent/session identifier checked by `run_containerized.sh` as a deployment gate; reference it in JSON config via `"${MCP_GATEWAY_AGENT_ID}"` to enable auth matching | `your-agent-id` |
| `MCP_GATEWAY_API_KEY` | Deprecated alias for `MCP_GATEWAY_AGENT_ID` (still accepted with warning) | (deprecated) |

## Optional (Non-Containerized Mode)

Expand All @@ -20,7 +21,8 @@ When running locally (`run.sh`), these variables are optional (warnings shown if
|----------|-------------|---------|
| `MCP_GATEWAY_PORT` | Port used by `run.sh` to build the `--listen` address; also read by `awmg --validate-env` for port-mapping checks | `8000` |
| `MCP_GATEWAY_DOMAIN` | Gateway domain | `localhost` |
| `MCP_GATEWAY_API_KEY` | Informational only — not read directly by the binary; must be referenced in your config via `"${MCP_GATEWAY_API_KEY}"` to enable authentication | (disabled) |
| `MCP_GATEWAY_AGENT_ID` | Informational only — not read directly by the binary; must be referenced in your config via `"${MCP_GATEWAY_AGENT_ID}"` to enable auth matching | (disabled) |
| `MCP_GATEWAY_API_KEY` | Deprecated alias for `MCP_GATEWAY_AGENT_ID` (still accepted with warning) | (deprecated) |
| `MCP_GATEWAY_LOG_DIR` | Log file directory (sets default for `--log-dir` flag) | `/tmp/gh-aw/mcp-logs` |
| `MCP_GATEWAY_WASM_CACHE_DIR` | Disk-backed wazero compilation cache directory (sets default for `--wasm-cache-dir`; defaults to `<parent-of-log-dir>/wazero-cache`, a sibling of the log directory) | `/tmp/gh-aw/wazero-cache` |
| `MCP_GATEWAY_PAYLOAD_DIR` | Large payload storage directory (sets default for `--payload-dir` flag). Must be an absolute path. | `/tmp/jq-payloads` |
Expand Down
2 changes: 1 addition & 1 deletion example-http-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
"gateway": {
"port": 3001,
"domain": "localhost",
"apiKey": "gateway-api-key"
"agentId": "gateway-agent-id"
}
}
52 changes: 36 additions & 16 deletions internal/auth/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
// without any scheme prefix (e.g., NOT "Bearer <key>").
//
// The package provides both full parsing with error handling (ParseAuthHeader)
// and convenience methods for specific use cases (ExtractAgentID, ValidateAPIKey).
// and convenience methods for specific use cases (ExtractAgentID, ValidateAgentID).
//
// Usage Guidelines:
//
Expand All @@ -16,7 +16,7 @@
// - Use ExtractAgentID() when you only need the agent ID and want automatic
// fallback to "default" instead of error handling.
//
// - Use ValidateAPIKey() to check if a provided key matches the expected value.
// - Use ValidateAgentID() to check if a provided identifier matches the expected value.
// Automatically handles the case where authentication is disabled (no expected key).
//
// Example:
Expand All @@ -26,8 +26,8 @@
// if err != nil {
// return err
// }
// if !auth.ValidateAPIKey(apiKey, expectedKey) {
// return errors.New("invalid API key")
// if !auth.ValidateAgentID(apiKey, expectedKey) {
// return errors.New("invalid agent ID")
// }
//
// // Extract agent ID only (for context, not authentication)
Expand Down Expand Up @@ -99,26 +99,31 @@ func ParseAuthHeader(authHeader string) (apiKey string, agentID string, error er

// Per MCP spec 7.1: Authorization header contains API key directly
// Use the entire header value as both API key and agent/session ID
log.Print("Using plain API key format (MCP spec 7.1)")
log.Print("Using plain agent ID format (MCP spec 7.1)")
return authHeader, authHeader, nil
}

// ValidateAPIKey checks if the provided API key matches the expected key.
// ValidateAgentID checks if the provided agent identifier matches the expected value.
// Returns true if they match, false otherwise.
func ValidateAPIKey(provided, expected string) bool {
log.Printf("Validating API key: expected_configured=%t", expected != "")
func ValidateAgentID(provided, expected string) bool {
log.Printf("Validating agent ID: expected_configured=%t", expected != "")

if expected == "" {
// No API key configured, authentication is disabled
log.Print("No API key configured, authentication disabled")
// No agent ID configured, authentication is disabled
log.Print("No agent ID configured, authentication disabled")
return true
}

matches := provided == expected
log.Printf("API key validation result: matches=%t", matches)
log.Printf("Agent ID validation result: matches=%t", matches)
return matches
}

// ValidateAPIKey is a deprecated alias for ValidateAgentID.
func ValidateAPIKey(provided, expected string) bool {
return ValidateAgentID(provided, expected)
}

// ExtractAgentID extracts the agent ID from an Authorization header.
// This is a convenience wrapper around ParseAuthHeader that only returns the agent ID.
// Returns "default" if the header is empty or cannot be parsed.
Expand Down Expand Up @@ -169,10 +174,25 @@ func ExtractSessionID(authHeader string) string {
}

// Plain format (per spec 7.1 - API key is session ID)
log.Print("Using plain API key as session ID")
log.Print("Using plain agent ID as session ID")
return authHeader
}

// ExtractSessionIDFromHeaders extracts session ID from X-Agent-ID and Authorization.
// X-Agent-ID takes precedence when present, otherwise Authorization is used.
func ExtractSessionIDFromHeaders(xAgentID, authHeader string) string {
if xAgentID != "" {
if IsMalformedHeader(xAgentID) {
return ""
}
return xAgentID
}
if IsMalformedHeader(authHeader) {
return ""
}
return ExtractSessionID(authHeader)
}

// IsMalformedHeader returns true if the header value contains characters
// that are not valid in HTTP header values per RFC 7230: null bytes, control
// characters below 0x20 (except horizontal tab 0x09), or DEL (0x7F).
Expand All @@ -190,12 +210,12 @@ func IsMalformedHeader(header string) bool {
// Per spec §7.3, the gateway SHOULD generate a random API key on startup
// if none is provided. Returns a 32-byte hex-encoded string (64 chars).
func GenerateRandomAPIKey() (string, error) {
logAPIKey.Print("Generating random API key")
logAPIKey.Print("Generating random agent ID")
key, err := strutil.RandomHex(32)
if err != nil {
logAPIKey.Printf("Random API key generation failed: %v", err)
return "", fmt.Errorf("failed to generate random API key: %w", err)
logAPIKey.Printf("Random agent ID generation failed: %v", err)
return "", fmt.Errorf("failed to generate random agent ID: %w", err)
}
logAPIKey.Print("Random API key generated successfully")
logAPIKey.Print("Random agent ID generated successfully")
return key, nil
}
Loading
Loading