Contributing to MCP Gateway
June 14, 2026 · View on GitHub
Thank you for your interest in contributing to MCP Gateway! This document provides guidelines and instructions for developers working on the project.
Prerequisites
- Docker installed and running
- Go 1.25.0 or later (see installation instructions)
- Make for running build commands
Getting Started
Initial Setup
-
Clone the repository
git clone https://github.com/github/gh-aw-mcpg.git cd gh-aw-mcpg -
Install toolchains and dependencies
make installThis will:
- Verify Go installation (and warn if the version is older than 1.25)
- Install golangci-lint if not present
- Download and verify Go module dependencies
-
Create a GitHub Personal Access Token
- Go to https://github.com/settings/tokens
- Click "Generate new token (classic)"
- Select scopes as needed (e.g.,
repofor repository access) - Copy the generated token
-
Create your Environment File
Replace the placeholder value with your actual token:
sed 's/GITHUB_PERSONAL_ACCESS_TOKEN=.*/GITHUB_PERSONAL_ACCESS_TOKEN=your_token_here/' example.env > .env -
Pull required Docker images
docker pull ghcr.io/github/github-mcp-server:latest docker pull mcp/fetch docker pull mcp/memory
Development Workflow
Building
Build the binary using:
make build
This creates the awmg binary in the project root.
Note
make build runs go mod tidy before go build. In network-restricted environments, ensure required modules are already cached or use direct go build -o awmg . when appropriate.
List all available Make targets:
make help
Testing
The test suite is split into two types:
Unit Tests (No Build Required)
Run unit tests that test code in isolation without needing the built binary:
make test # Alias for test-unit
make test-unit # Run only unit tests (./internal/... packages)
Run unit tests with coverage:
make coverage
For CI environments with JSON output:
make test-ci
Integration Tests (Build Required)
Run binary integration tests that require a built binary:
make test-integration # Automatically builds binary if needed
Run All Tests
Run both unit and integration tests (always rebuilds the binary first):
make test-all
Rust Guard Tests
Run Rust guard unit tests (requires cargo):
make test-rust
Install the Rust toolchain from rustup.rs if not already present.
Serena MCP Tests (Optional)
Run Serena MCP Server tests (requires Docker and network access):
make test-serena # Direct connection tests
make test-serena-gateway # Tests routed via the MCP Gateway
Container Proxy Tests (Optional)
Run container proxy integration tests (requires Docker and gh CLI or a GITHUB_TOKEN/GH_TOKEN environment variable):
make test-container-proxy
This target builds a Docker image and tests proxy mode with TLS. It requires a GitHub token available via the gh CLI (gh auth login) or the GITHUB_TOKEN/GH_TOKEN environment variable.
Testing Environment Variables
AWMG_BINARY_PATH— Override the binary path used by integration tests when you want to run tests against a prebuiltawmgbinary.AWMG_WASM_GUARD_PATH— Override the GitHub guard WASM path used by proxy integration tests when the default build output path is not available.
Race Detection Tests
Run unit tests with Go's race detector to catch concurrent data races:
make test-race
The MCP Gateway is a concurrent server; use this to validate thread safety when modifying concurrent code.
Linting
Run all linters (go vet, gofmt check, and golangci-lint if installed; v2.8.0 is the recommended version):
make lint
This runs:
go vetfor common code issuesgofmtcheck for code formattinggolangci-lintfor additional static analysis (misspell, unconvert)
Note: make install installs golangci-lint v2.8.0 only when golangci-lint is not already found on your PATH/GOPATH. If another version is already installed, make lint uses that existing binary.
To run golangci-lint directly with all configured linters:
golangci-lint run --timeout=5m
Formatting
Auto-format code using gofmt:
make format
Running Locally
Start the server with:
./run.sh
This will start MCPG in routed mode on http://0.0.0.0:8000 (using the defaults from run.sh).
Or run manually:
# Run with TOML config
./awmg --config config.toml
# Run with JSON stdin config
echo '{"mcpServers": {...}}' | ./awmg --config-stdin
Advanced Flags
# Custom log directory
./awmg --config config.toml --log-dir /path/to/logs
# Load environment file
./awmg --config config.toml --env .env
# Increase verbosity (-v=info, -vv=debug, -vvv=trace)
./awmg --config config.toml -vv
# Launch MCP servers sequentially during startup
./awmg --config config.toml --sequential-launch
# Custom payload directory and size threshold (payload dir must be absolute)
./awmg --config config.toml --payload-dir /tmp/payloads --payload-size-threshold 1048576
# Enable OTLP tracing with a 25% sample rate
./awmg --config config.toml --otlp-endpoint http://localhost:4318 --otlp-sample-rate 0.25
See docs/ENVIRONMENT_VARIABLES.md for the full list of environment variable overrides.
Testing with Codex
You can test MCPG with Codex (in another terminal):
cp ~/.codex/config.toml ~/.codex/config.toml.bak && cp agent-configs/codex.config.toml ~/.codex/config.toml
AGENT_ID=demo-agent codex
You can use '/mcp' in codex to list the available tools.
When you're done you can restore your old codex config file:
cp ~/.codex/config.toml.bak ~/.codex/config.toml
Testing with curl
You can test the MCP server directly using curl commands:
Note: The examples below use port 3000, which is the default when running the binary directly (
./awmg --config config.toml). If you started the server with./run.sh, the default port is 8000 — updateMCP_URLaccordingly (e.g.,http://127.0.0.1:8000/mcp/github).
Without API Key (session tracking only)
MCP_URL="http://127.0.0.1:3000/mcp/github"
# Initialize
curl -X POST $MCP_URL \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Authorization: demo-session-id' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1.0.0","capabilities":{},"clientInfo":{"name":"curl","version":"0.1"}}}'
# List tools
curl -X POST $MCP_URL \
-H 'Content-Type: application/json' \
-H 'Authorization: demo-session-id' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
With API Key (authentication enabled)
MCP_URL="http://127.0.0.1:3000/mcp/github"
API_KEY="your-api-key-here"
# Initialize (per spec 7.1: Authorization header contains plain API key, NOT Bearer scheme)
curl -X POST $MCP_URL \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H "Authorization: $API_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1.0.0","capabilities":{},"clientInfo":{"name":"curl","version":"0.1"}}}'
# List tools
curl -X POST $MCP_URL \
-H 'Content-Type: application/json' \
-H "Authorization: $API_KEY" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
Cleaning
Remove build artifacts:
make clean
Project Structure
gh-aw-mcpg/
├── main.go # Entry point
├── go.mod # Dependencies
├── Dockerfile # Container image
├── Makefile # Build automation
└── internal/
├── auth/ # Authentication header parsing and middleware
├── cmd/ # CLI commands (cobra)
├── config/ # Configuration loading (TOML/JSON)
├── difc/ # Decentralized Information Flow Control
├── envutil/ # Environment variable utilities
├── guard/ # Security guards (NoopGuard, WasmGuard, WriteSinkGuard)
├── httputil/ # Shared HTTP helper utilities (server responses, proxy transport)
├── launcher/ # Backend server management
├── logger/ # Debug logging framework
├── mcp/ # MCP protocol types & connection
├── mcpresult/ # MCP result text content helpers
├── middleware/ # HTTP middleware (jq schema processing)
├── oidc/ # OIDC authentication for HTTP MCP backends
├── proxy/ # HTTP forward proxy for DIFC filtering
├── sanitize/ # Sensitive data redaction utilities for logging
├── server/ # HTTP server (routed/unified modes)
├── strutil/ # String and formatting utility helpers (deduplication, trimming, duration formatting, JSON deep-clone)
├── syncutil/ # Concurrency utility helpers
├── sys/ # System utilities
├── testutil/ # Test utilities and helpers
├── tracing/ # OpenTelemetry OTLP tracing helpers
├── tty/ # Terminal detection utilities
└── version/ # Version management
Key Directories
internal/auth/- Authentication header parsing and middlewareinternal/cmd/- CLI implementation using Cobra frameworkinternal/config/- Configuration parsing for TOML and JSON formatsinternal/difc/- Decentralized Information Flow Controlinternal/envutil/- Environment variable utilitiesinternal/guard/- Guard framework for resource labelinginternal/httputil/- Shared HTTP helper utilities (server responses, proxy transport)internal/launcher/- Backend process management (Docker, stdio)internal/logger/- Micro logger for debug outputinternal/mcp/- MCP protocol types and JSON-RPC handlinginternal/mcpresult/- MCP result text content helpersinternal/middleware/- HTTP middleware (jq schema processing)internal/oidc/- OIDC authentication for HTTP MCP backendsinternal/proxy/- HTTP forward proxy applying DIFC filtering toghCLI and REST/GraphQL requestsinternal/sanitize/- Sensitive data redaction utilities (SanitizeString,SanitizeJSON,TruncateSecret) for safe log outputinternal/server/- HTTP server with routed and unified modesinternal/strutil/- String and formatting utility helpers (deduplication, trimming, duration formatting, JSON deep-clone)internal/syncutil/- Concurrency utility helpers (get-or-create pattern)internal/sys/- System utilitiesinternal/testutil/- Test utilities and helpersinternal/tracing/- OpenTelemetry OTLP trace export helpers (HTTP handler wrapping, provider management)internal/tty/- Terminal detection utilitiesinternal/version/- Version management
Coding Conventions
Go Style Guidelines
- Follow standard Go conventions (see Effective Go)
- Use internal packages in
internal/for non-exported code - Test files:
*_test.gowith table-driven tests - Naming:
camelCasefor private/unexported identifiersPascalCasefor public/exported identifiers
- Always handle errors explicitly
- Add Godoc comments for all exported functions, types, and packages
- Mock external dependencies (Docker, network) in tests
Constructor Naming Conventions
The codebase uses three distinct constructor patterns. Follow these conventions consistently:
1. New*(args) *Type - Standard Constructors
Use for simple object creation without error handling or complex initialization.
// Creates a new instance of the type directly
func NewConnection(ctx context.Context) *Connection { ... }
func NewRegistry() *Registry { ... }
func NewSession(sessionID, token string) *Session { ... }
When to use:
- Object creation is always successful (no errors to return)
- Direct instantiation of struct with provided parameters
- Most common pattern in the codebase (35+ usages)
2. Create*(args) (Type, error) - Factory Patterns
Use for factory functions that perform registry lookups or complex configuration-based initialization.
// Looks up a guard type from registry and creates it
func CreateGuard(name string) (Guard, error) { ... }
// Complex initialization with potential failures
func CreateHTTPServerForMCP(cfg *Config) (*http.Server, error) { ... }
When to use:
- Registry-based object creation (looking up registered types)
- Complex configuration that might fail
- Need to validate parameters and return errors
- Factory pattern with type selection logic
3. Init*(args) error - Global State Initialization
Use for initializing global singletons, loggers, or package-level state.
// Initializes global file logger singleton
func InitFileLogger(dir string) error { ... }
// Initializes global JSON logger singleton
func InitJSONLLogger(dir string) error { ... }
When to use:
- Initializing global variables or package-level state
- Singleton initialization that should only happen once
- Setting up loggers, configuration, or other shared resources
- Typically returns an error if initialization fails
Examples from Codebase
Standard Constructors (New*):
NewConnection,NewHTTPConnection(mcp package)NewUnified,NewSession(server package)NewRegistry,NewNoopGuard(guard package)NewLabel,NewAgentLabels(difc package)
Factory Patterns (Create*):
CreateGuard(guard package) - registry lookupCreateHTTPServerForMCP(server package) - complex config-based creation
Global Initialization (Init*):
InitFileLogger,InitJSONLLogger,InitMarkdownLogger,InitServerFileLogger(logger package)
When in doubt: Use New* for most constructors. Only use Create* when implementing factory patterns with type selection, and Init* for global state initialization.
Debug Logging
Use the logger package for debug logging:
import "github.com/github/gh-aw-mcpg/internal/logger"
// Create a logger with namespace following pkg:filename convention
// Use descriptive variable names (e.g., logLauncher, logConfig) for clarity
var logComponent = logger.New("pkg:filename")
// Log debug messages (only shown when DEBUG environment variable matches)
logComponent.Printf("Processing %d items", count)
// Check if logging is enabled before expensive operations
if logComponent.Enabled() {
logComponent.Printf("Expensive debug info: %+v", expensiveOperation())
}
Logger Variable Naming Convention:
- Prefer descriptive names:
var log<Component> = logger.New("pkg:component") - Examples:
var logLauncher = logger.New("launcher:launcher") - Avoid generic
logwhen it might conflict with standard library - Capitalize the component part after 'log' (e.g.,
logAuthwith capital 'A',logLauncherwith capital 'L')
Control debug output:
DEBUG=* ./awmg --config config.toml # Enable all
DEBUG=server:* ./awmg --config config.toml # Enable specific package
Dependencies
The project uses:
github.com/spf13/cobra- CLI frameworkgithub.com/BurntSushi/toml- TOML parsergithub.com/modelcontextprotocol/go-sdk- MCP protocol implementationgithub.com/itchyny/gojq- JQ schema processinggithub.com/santhosh-tekuri/jsonschema/v5- JSON schema validationgithub.com/stretchr/testify- Test assertionsgithub.com/tetratelabs/wazero- WASM runtime for executing WASM-based security guardsgo.opentelemetry.io/otel- OpenTelemetry tracing API and span/trace managementgolang.org/x/term- Terminal detection- Standard library for JSON, HTTP, exec
To add a new dependency:
go get <package>
go mod tidy
Testing
Test Structure
The project has two types of tests:
-
Unit Tests (in
internal/packages)- Test code in isolation without requiring a built binary
- Run quickly and don't need Docker or external dependencies
- Located in
*_test.gofiles alongside source code
-
Integration Tests (in
test/integration/)- Test the compiled
awmgbinary end-to-end - Require building the binary first (
make build) - Test actual server behavior, command-line flags, and real process execution
- Test the compiled
Running Tests
# Run unit tests only (fast, no build needed)
make test # Alias for test-unit
make test-unit
# Run integration tests (requires binary build)
make test-integration
# Run all tests (unit + integration)
make test-all
# Run unit tests with race detection
make test-race
# Run unit tests with coverage
make coverage
# Run specific package tests
go test ./internal/server/...
Race Detection Tests
The MCP Gateway is a concurrent HTTP server, so race detection is especially important when modifying server, launcher, or guard code:
make test-race
This runs go test -race across all internal packages to catch data races in
concurrent code paths.
Writing Tests
- Place tests in
*_test.gofiles alongside the code - Use table-driven tests for multiple test cases
- Mock external dependencies (Docker API, network calls)
- Follow existing test patterns in the codebase
Example:
func TestMyFunction(t *testing.T) {
tests := []struct {
name string
input string
want string
wantErr bool
}{
// test cases...
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// test implementation...
})
}
}
Docker Development
Build Image
docker build -t awmg .
Run Container
docker run --rm -i \
-e MCP_GATEWAY_PORT=8000 \
-e MCP_GATEWAY_DOMAIN=localhost \
-e MCP_GATEWAY_AGENT_ID=your-agent-id \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8000:8000 \
awmg < config.json
The container uses run_containerized.sh as the entrypoint, which:
- Requires the
-iflag for JSON configuration via stdin - Requires
MCP_GATEWAY_PORTandMCP_GATEWAY_DOMAIN, plus an agent gate value viaMCP_GATEWAY_AGENT_ID(MCP_GATEWAY_API_KEYis only a deprecated alias thatrun_containerized.shmaps toMCP_GATEWAY_AGENT_ID; reference"gateway": {"agentId": "${MCP_GATEWAY_AGENT_ID}"}in your JSON config to enable authentication) - Queries the Docker daemon API version (falls back to 1.44)
- Validates Docker socket, port mapping, and environment before starting
See config.json for an example JSON configuration file.
Override with custom configuration
To use a different config file or adjust settings:
docker run --rm -i \
-e MCP_GATEWAY_PORT=8080 \
-e MCP_GATEWAY_DOMAIN=example.com \
-e MCP_GATEWAY_AGENT_ID=your-agent-id \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8080:8080 \
awmg < custom-config.json
Required environment variables:
MCP_GATEWAY_PORT- Server port (must match port mapping)MCP_GATEWAY_DOMAIN- Domain name for the gatewayMCP_GATEWAY_AGENT_ID- Checked byrun_containerized.shas a deployment gate; must be referenced in your JSON config via"gateway": {"agentId": "${MCP_GATEWAY_AGENT_ID}"}to enable authentication (MCP_GATEWAY_API_KEYis only a deprecated alias mapped to this value)
Note: The DOCKER_API_VERSION is set automatically by run_containerized.sh using the Docker daemon's current API version (falls back to 1.44 for all architectures if detection fails).
Pull Request Guidelines
- Create a feature branch from
main - Make focused commits with clear commit messages
- Add tests for new functionality
- Run linters and tests before submitting:
make agent-finished # format + build + lint + go test ./... + Rust guard tests - Update documentation if you change behavior or add features
- Keep changes minimal - smaller PRs are easier to review
Creating a Release
Releases are created using semantic versioning tags (e.g., v1.2.3). The make release command triggers the automated release workflow:
# Create a patch release (v1.2.3 -> v1.2.4)
make release patch
# Create a minor release (v1.2.3 -> v1.3.0)
make release minor
# Create a major release (v1.2.3 -> v2.0.0)
make release major
Prerequisites:
- The
ghCLI must be installed and authenticated (gh auth login) - You must have permission to trigger workflows in the repository
Release Process
-
Run the release command with the appropriate bump type:
make release patch -
Review the version that will be created:
Latest tag: v1.2.3 Next version will be: v1.2.4 Do you want to trigger the release workflow? [Y/n] -
Confirm by pressing
Y(orEnterfor yes) -
Monitor the workflow at the URL shown:
✓ Release workflow triggered successfully The workflow will: 1. Run tests to ensure everything passes 2. Create and push tag: v1.2.4 3. Build multi-platform binaries 4. Build and push Docker containers 5. Generate SBOMs 6. Create GitHub release with artifacts Monitor the release workflow at: https://github.com/github/gh-aw-mcpg/actions/workflows/release.lock.yml
What Happens Automatically
When the release workflow is triggered, it automatically:
- Runs the full test suite (unit + integration)
- Creates and pushes the version tag (e.g.,
v1.2.4) - Builds multi-platform binaries (Linux for amd64, arm, and arm64)
- Creates a GitHub release with all binaries and checksums
- Builds and pushes a multi-arch Docker image to
ghcr.io/github/gh-aw-mcpgwith tags:latest- Always points to the newest releasev1.2.4- Specific version tag<commit-sha>- Specific commit reference
- Generates and attaches SBOM files (SPDX and CycloneDX formats)
- Creates release highlights from merged PRs
Version Guidelines
- Patch (
v1.2.3→v1.2.4): Bug fixes, documentation updates, minor improvements - Minor (
v1.2.3→v1.3.0): New features, non-breaking changes - Major (
v1.2.3→v2.0.0): Breaking changes, major architectural changes
Architecture Notes
Core Features
- JSON stdin configuration with
${VAR}variable expansion - TOML configuration loaded from file (
${VAR}expansion supported only in[gateway.opentelemetry]section fields:endpoint,trace_id,span_id,headers) - Stdio transport for backend servers (containerized via Docker)
- Docker container launching
- Routed mode: Each backend at
/mcp/{serverID} - Unified mode: All backends at
/mcp - HTTP forward proxy mode (
awmg proxy) with DIFC filtering forghCLI and REST/GraphQL requests - Basic request/response proxying
- WASM-based DIFC guards (
internal/guard/) withallow-onlyandwrite-sinkguard policies - OIDC authentication for HTTP MCP backends
- Large payload handling with configurable size threshold and disk storage
- Per-server and unified file logging (
.log,gateway.md,rpc-messages.jsonl,tools.json) - Health endpoint at
GET /healthreturning structured JSON --validate-envflag for environment pre-validation
See README.md for the full feature set and architecture overview.
Questions or Issues?
- Check existing issues
- Open a new issue with a clear description
- Join discussions in pull requests
License
MIT License - see LICENSE file for details.