Thank you for your interest in contributing to MCP Gateway! This document provides guidelines and instructions for developers working on the project.
- Docker installed and running
- Go 1.25.0 (see installation instructions)
- Make for running build commands
-
Clone the repository
git clone https://github.com/github/gh-aw-mcpg.git cd gh-aw-mcpg -
Install toolchains and dependencies
make install
This will:
- Verify Go installation (and warn if version doesn't match 1.25.0)
- 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
Build the binary using:
make buildThis creates the awmg binary in the project root.
List all available Make targets:
make helpThe test suite is split into two types:
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 coverageFor CI environments with JSON output:
make test-ciRun binary integration tests that require a built binary:
make test-integration # Automatically builds binary if neededRun both unit and integration tests (always rebuilds the binary first):
make test-allRun Rust guard unit tests (requires cargo):
make test-rustInstall the Rust toolchain from rustup.rs if not already present.
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 GatewayRun container proxy integration tests (requires Docker and gh CLI or a GITHUB_TOKEN/GH_TOKEN environment variable):
make test-container-proxyThis 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.
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.
Run unit tests with Go's race detector to catch concurrent data races:
make test-raceThe MCP Gateway is a concurrent server; use this to validate thread safety when modifying concurrent code.
Run all linters (go vet, gofmt check, and golangci-lint if installed; v2.8.0 is the recommended version):
make lintThis 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=5mAuto-format code using gofmt:
make formatStart the server with:
./run.shThis 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# 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 1048576See docs/ENVIRONMENT_VARIABLES.md for the full list of environment variable overrides.
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 codexYou 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.tomlYou 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).
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":{}}'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":{}}'Remove build artifacts:
make cleangh-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
├── middleware/ # HTTP middleware (jq schema processing)
├── oidc/ # OIDC authentication for HTTP MCP backends
├── proxy/ # HTTP forward proxy for DIFC filtering
├── server/ # HTTP server (routed/unified modes)
├── strutil/ # String and formatting utility helpers
├── syncutil/ # Concurrency utility helpers
├── sys/ # System utilities
├── testutil/ # Test utilities and helpers
├── tracing/ # OpenTelemetry OTLP tracing helpers
├── tty/ # Terminal detection utilities
└── version/ # Version management
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/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/server/- HTTP server with routed and unified modesinternal/strutil/- String and formatting utility helpers (deduplication, trimming, duration formatting)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
- 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
The codebase uses three distinct constructor patterns. Follow these conventions consistently:
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)
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
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
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.
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 packageThe project uses:
github.com/spf13/cobra- CLI frameworkgithub.com/spf13/pflag- POSIX/GNU-style flag parsing used by tracing and CLI integrationsgithub.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 tidyThe 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
# 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/...The MCP Gateway is a concurrent HTTP server, so race detection is especially important when modifying server, launcher, or guard code:
make test-raceThis runs go test -race across all internal packages to catch data races in
concurrent code paths.
- 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 build -t awmg .docker run --rm -i \
-e MCP_GATEWAY_PORT=8000 \
-e MCP_GATEWAY_DOMAIN=localhost \
-e MCP_GATEWAY_API_KEY=your-secret-key \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8000:8000 \
awmg < config.jsonThe container uses run_containerized.sh as the entrypoint, which:
- Requires the
-iflag for JSON configuration via stdin - Requires
MCP_GATEWAY_PORT,MCP_GATEWAY_DOMAIN,MCP_GATEWAY_API_KEYenv vars (the API key is a deployment gate; reference it in your JSON config via"gateway": {"apiKey": "${MCP_GATEWAY_API_KEY}"}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.
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_API_KEY=your-secret-key \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 8080:8080 \
awmg < custom-config.jsonRequired environment variables:
MCP_GATEWAY_PORT- Server port (must match port mapping)MCP_GATEWAY_DOMAIN- Domain name for the gatewayMCP_GATEWAY_API_KEY- Checked byrun_containerized.shas a deployment gate; must be referenced in your JSON config via"gateway": {"apiKey": "${MCP_GATEWAY_API_KEY}"}to enable authentication
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).
- 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
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 majorPrerequisites:
- The
ghCLI must be installed and authenticated (gh auth login) - You must have permission to trigger workflows in the repository
-
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
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
- 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
- JSON stdin configuration with
${VAR}variable expansion - TOML configuration loaded from file (no
${VAR}variable expansion) - 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.
- Check existing issues
- Open a new issue with a clear description
- Join discussions in pull requests
MIT License - see LICENSE file for details.