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.
The 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:
make test-allRun all linters (go vet, gofmt check, and golangci-lint):
make lintThis runs:
go vetfor common code issuesgofmtcheck for code formattinggolangci-lintfor additional static analysis (misspell, unconvert)
Note: golangci-lint is automatically installed by make install. If you see a warning about golangci-lint not being found, run make install first.
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-stdinYou 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:
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 cleanawmg/
├── 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/ # Data Information Flow Control
├── envutil/ # Environment variable utilities
├── guard/ # Security guards (NoopGuard active)
├── launcher/ # Backend server management
├── logger/ # Debug logging framework
├── mcp/ # MCP protocol types & connection
├── middleware/ # HTTP middleware (jq schema processing)
├── server/ # HTTP server (routed/unified modes)
├── sys/ # System utilities
├── testutil/ # Test utilities and helpers
├── timeutil/ # Time formatting utilities
├── 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/- Data Information Flow Controlinternal/envutil/- Environment variable utilitiesinternal/guard/- Guard framework for resource labelinginternal/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/server/- HTTP server with routed and unified modesinternal/sys/- System utilitiesinternal/testutil/- Test utilities and helpersinternal/timeutil/- Time formatting utilitiesinternal/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.1485827954.workers.dev/BurntSushi/toml- TOML parsergithub.1485827954.workers.dev/modelcontextprotocol/go-sdk- MCP protocol implementationgithub.1485827954.workers.dev/itchyny/gojq- JQ schema processinggithub.1485827954.workers.dev/santhosh-tekuri/jsonschema/v5- JSON schema validationgithub.1485827954.workers.dev/stretchr/testify- Test assertionsgolang.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 coverage
make coverage
# Run specific package tests
go test ./internal/server/...- 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 - 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- API key for 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 lint make test - 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
- TOML and JSON stdin configuration
- Stdio transport for backend servers
- Docker container launching
- Routed mode: Each backend at
/mcp/{serverID} - Unified mode: All backends at
/mcp - Basic request/response proxying
- Check existing issues
- Open a new issue with a clear description
- Join discussions in pull requests
MIT License - see LICENSE file for details.