Quick reference for AI agents working with MCP Gateway (Go-based MCP proxy server).
Install: make install (install toolchains and dependencies)
Build: make build (builds awmg binary)
Test: make test (run unit tests, no build required)
Test-Unit: make test-unit (run unit tests only)
Test-Integration: make test-integration (run binary integration tests, requires build)
Test-All: make test-all (run both unit and integration tests)
Lint: make lint (runs go vet, gofmt checks, and golangci-lint)
Coverage: make coverage (unit tests with coverage report)
Format: make format (auto-format code with gofmt)
Clean: make clean (remove build artifacts)
Agent-Finished: make agent-finished (run format, build, lint, and all tests - ALWAYS run before completion)
Run: ./awmg --config config.toml
Run with Custom Log Directory: ./awmg --config config.toml --log-dir /path/to/logs
Run with Custom Payload Directory: ./awmg --config config.toml --payload-dir /path/to/payloads
internal/auth/- Authentication header parsing and middlewareinternal/cmd/- CLI (Cobra)internal/config/- Config parsing (TOML/JSON) with validationvalidation.go- Variable expansion and fail-fast validationvalidation_test.go- 21 comprehensive validation tests
internal/difc/- Data Information Flow Controlinternal/envutil/- Environment variable utilitiesinternal/guard/- Security guards (NoopGuard active)internal/launcher/- Backend process managementinternal/logger/- Debug logging framework (micro logger)internal/mcp/- MCP protocol types with enhanced error logginginternal/middleware/- HTTP middleware (jq schema processing)internal/server/- HTTP server (routed/unified modes)internal/sys/- System utilitiesinternal/testutil/- Test utilities and helpersinternal/timeutil/- Time formatting utilitiesinternal/tty/- Terminal detection utilitiesinternal/version/- Version management
- Go 1.25.0 with
cobra,toml,go-sdk - Protocol: JSON-RPC 2.0 over stdio
- Routing:
/mcp/{serverID}(routed) or/mcp(unified) - Docker: Launches MCP servers as containers
- Validation: Spec-compliant with fail-fast error handling
- Variable Expansion:
${VAR_NAME}syntax for environment variables
Configuration Spec: See MCP Gateway Configuration Reference for complete specification.
TOML (config.toml):
[gateway]
port = 3000
api_key = "your-api-key"
payload_dir = "/tmp/jq-payloads" # Optional: directory for large payload storage
[servers.github]
command = "docker"
args = ["run", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "-i", "ghcr.io/github/github-mcp-server:latest"]JSON (stdin):
{
"mcpServers": {
"github": {
"type": "stdio",
"container": "ghcr.io/github/github-mcp-server:latest",
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "",
"CONFIG_PATH": "${GITHUB_CONFIG_DIR}"
}
}
}
}Supported Types: "stdio", "http" (fully supported), "local" (alias for stdio)
Validation Features:
- Environment variable expansion:
${VAR_NAME}(fails if undefined) - Required fields:
containerfor stdio,urlfor http - Containerization Requirement: TOML stdio servers must use
command = "docker"per MCP Gateway Specification Section 3.2.1 - Note: In JSON stdin format, the
commandfield is not supported - stdio servers must usecontainerfield - Port range validation: 1-65535
- Timeout validation: positive integers only
- Internal packages in
internal/ - Test files:
*_test.gowith table-driven tests - Naming: camelCase (private), PascalCase (public)
- Always handle errors explicitly
- Godoc comments for exports
- Mock external dependencies (Docker, network)
ALWAYS use testify for test assertions - The project uses stretchr/testify for all test assertions.
require: Use for critical checks - test stops on failureassert: Use for non-critical checks - test continues on failure
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestExample(t *testing.T) {
result, err := DoSomething()
require.NoError(t, err) // Stop if error - can't continue
assert.Equal(t, "expected", result.Field) // Continue even if fails
}For tests with multiple assertions, use bound asserters to reduce repetition:
func TestMultipleAssertions(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
result := GetResult()
require.NotNil(result) // Stop if nil
// Cleaner - no need to pass 't' repeatedly
assert.Equal("value1", result.Field1)
assert.Equal("value2", result.Field2)
assert.True(result.Active)
}Use specific assertion methods instead of generic ones for better error messages:
// ❌ Avoid generic assertions
assert.True(t, len(items) == 0)
assert.True(t, err == nil)
assert.True(t, strings.Contains(msg, "error"))
// ✅ Use specific assertions
assert.Empty(t, items)
assert.NoError(t, err)
assert.Contains(t, msg, "error")// Length checking
assert.Len(t, items, 5, "Expected 5 items")
// Unordered slice comparison
assert.ElementsMatch(t, expected, actual, "Slices should contain same elements")
// Nil checking
assert.NotNil(t, obj, "Object should not be nil")
assert.Nil(t, err, "Error should be nil")
// Error checking (prefer NoError over Nil for errors)
assert.NoError(t, err, "Operation should succeed")
assert.Error(t, err, "Operation should fail")
// HTTP status codes
assert.Equal(t, http.StatusOK, response.StatusCode, "Should return 200 OK")
// JSON comparison (ignores formatting)
assert.JSONEq(t, expectedJSON, actualJSON)golangci-lint is integrated and runs as part of make lint:
- Configuration:
.golangci.yml(version 2 format) - Enabled linters:
misspell,unconvert - Disabled linters:
gosec,testifylint,errcheck,gocritic,revive - Install:
make install(installs golangci-lint v2.8.0) - Run manually:
golangci-lint run --timeout=5m
testifylint: Available but disabled due to requiring extensive test refactoring across the codebase.
- Automatically catches common testify mistakes:
- Suggests
assert.Empty(t, x)instead ofassert.True(t, len(x) == 0) - Suggests
assert.True(t, x)instead ofassert.Equal(t, true, x) - Suggests
assert.NoError(t, err)instead ofassert.Nil(t, err)
- Suggests
- To run on specific files:
golangci-lint run --enable=testifylint --timeout=5m <files> - To run on entire codebase:
golangci-lint run --enable=testifylint --timeout=5m
Note: Some linters (gosec, testifylint, errcheck) are disabled to minimize noise. Enable them for stricter checks:
golangci-lint run --enable=gosec,testifylint,errcheck --timeout=5mUnit Tests (internal/ packages):
- Run without building the binary
- Test code in isolation with mocks
- Fast execution, no external dependencies
- Run with:
make testormake test-unit
Integration Tests (test/integration/):
- Test the compiled
awmgbinary end-to-end - Require building the binary first
- Test actual server behavior and CLI flags
- Run with:
make test-integration
All Tests: make test-all runs both unit and integration tests
Add MCP Server: Update config.toml with new server entry
Add Route: Edit internal/server/routed.go or unified.go
Add Guard: Implement in internal/guard/ and register
Add Auth Logic: Implement in internal/auth/ package
Add Unit Test: Create *_test.go in the appropriate internal/ package
Add Integration Test: Create test in test/integration/ that uses the binary
CRITICAL: Before returning to the user, ALWAYS run make agent-finished
This command runs the complete verification pipeline:
- Format - Auto-formats all Go code with gofmt
- Build - Ensures the project compiles successfully
- Lint - Runs go vet and gofmt checks
- Test All - Executes the full test suite (unit + integration tests)
Requirements:
- ALL failures must be fixed before completion
- If
make agent-finishedfails at any stage, debug and fix the issue - Re-run
make agent-finishedafter fixes to verify success - Only report completion to the user after seeing "✓ All agent-finished checks passed!"
Example workflow:
# Make your code changes
# ...
# Run verification before completion
make agent-finished
# If any step fails, fix the issues and run again
# Only complete the task after all checks passALWAYS 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
var log = logger.New("pkg:filename")
// Log debug messages
// - Writes to stderr with colors and time diffs (when DEBUG matches namespace)
// - Also writes to file logger as text-only (always, when logger is enabled)
log.Printf("Processing %d items", count)
log.Print("Simple debug message")
// Check if logging is enabled before expensive operations
if log.Enabled() {
log.Printf("Expensive debug info: %+v", expensiveOperation())
}For operational/file logging, use the file logger directly:
import "github.com/github/gh-aw-mcpg/internal/logger"
// Log operational events (written to mcp-gateway.log)
logger.LogInfo("category", "Operation completed successfully")
logger.LogWarn("category", "Potential issue detected: %s", issue)
logger.LogError("category", "Operation failed: %v", err)
logger.LogDebug("category", "Debug details: %+v", details)Note: Debug loggers created with logger.New() now write to both stderr (with colors/time diffs) and the file logger (text-only). This provides real-time colored output during development while ensuring all debug logs are captured to file for production troubleshooting.
Logging Categories:
startup- Gateway initialization and configurationshutdown- Graceful shutdown eventsclient- MCP client interactions and requestsbackend- Backend MCP server operationsauth- Authentication events (success and failures)
Category Naming Convention:
- Follow the pattern:
pkg:filename(e.g.,server:routed,launcher:docker) - Use colon (
:) as separator between package and file/component name - Be consistent with existing loggers in the codebase
Logger Variable Naming Convention:
- Use descriptive names that match the component:
var log<Component> = logger.New("pkg:component") - Examples:
var logLauncher = logger.New("launcher:launcher"),var logConfig = logger.New("config:config") - Avoid generic
logname when it might conflict with standard library or when the file already importslogpackage - Capitalize the component part after 'log' (e.g.,
logAuthwith capital 'A',logLauncherwith capital 'L') - This convention makes it clear which logger is being used and reduces naming collisions
- For components with very short files or temporary code, generic
logis acceptable but descriptive is preferred
Examples of good logger naming:
// Descriptive - clearly indicates the component (RECOMMENDED)
var logLauncher = logger.New("launcher:launcher")
var logPool = logger.New("launcher:pool")
var logConfig = logger.New("config:config")
var logValidation = logger.New("config:validation")
var logUnified = logger.New("server:unified")
var logRouted = logger.New("server:routed")
// Generic - acceptable for simple cases but less clear
var log = logger.New("auth:header")
var log = logger.New("sys:sys")Debug Output Control:
# Enable all debug logs
DEBUG=* ./awmg --config config.toml
# Enable specific package
DEBUG=server:* ./awmg --config config.toml
# Enable multiple packages
DEBUG=server:*,launcher:* ./awmg --config config.toml
# Exclude specific loggers
DEBUG=*,-launcher:test ./awmg --config config.toml
# Disable colors (auto-disabled when piping)
DEBUG_COLORS=0 DEBUG=* ./awmg --config config.tomlKey Features:
- Zero overhead: Logs only computed when DEBUG matches the logger's namespace
- Time diff: Shows elapsed time between log calls (e.g.,
+50ms,+2.5s) - Auto-colors: Each namespace gets a consistent color in terminals
- Pattern matching: Supports wildcards (
*) and exclusions (-pattern)
When to Use:
- Non-essential diagnostic information
- Performance insights and timing data
- Internal state tracking during development
- Detailed operation flow for debugging
When NOT to Use:
- Essential user-facing messages (use standard logging)
- Error messages (use proper error handling)
- Final output or results (use stdout)
GITHUB_PERSONAL_ACCESS_TOKEN- GitHub authDOCKER_API_VERSION- Set by querying Docker daemon's current API version; falls back to1.44for all architectures if detection failsDEBUG- 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-dirflag, default:/tmp/gh-aw/mcp-logs)MCP_GATEWAY_PAYLOAD_DIR- Large payload storage directory (sets default for--payload-dirflag, default:/tmp/jq-payloads)
Note: The PORT, HOST, and MODE environment variables are used only by test scripts and are not read by the gateway application. The gateway uses command-line flags instead: --listen for bind address and --routed/--unified for mode selection.
File Logging:
- Operational logs are always written to log files in the configured log directory
- Default log directory:
/tmp/gh-aw/mcp-logs(configurable via--log-dirflag orMCP_GATEWAY_LOG_DIRenv var) - Falls back to stdout if log directory cannot be created
- Log Files Created:
mcp-gateway.log- Unified log with all messages{serverID}.log- Per-server logs (e.g.,github.log,slack.log) for easier troubleshootinggateway.md- Markdown-formatted logs for GitHub workflow previewsrpc-messages.jsonl- Machine-readable JSONL format for RPC message analysistools.json- Available tools from all backend MCP servers (mapping server IDs to their tool names and descriptions)
- Logs include: startup, client interactions, backend operations, auth events, errors
Per-ServerID Logging:
- Each backend MCP server gets its own log file for easier troubleshooting
- Use
LogInfoWithServer,LogWarnWithServer,LogErrorWithServer,LogDebugWithServerfunctions - Example:
logger.LogInfoWithServer("github", "backend", "Server started successfully") - Logs are written to both the server-specific file and the unified
mcp-gateway.log - Thread-safe concurrent logging with automatic fallback
Large Payload Handling:
- Large tool response payloads are stored in the configured payload directory
- Default payload directory:
/tmp/jq-payloads(configurable via--payload-dirflag,MCP_GATEWAY_PAYLOAD_DIRenv var, orpayload_dirin config) - Payloads are organized by session ID:
{payload_dir}/{sessionID}/{queryID}/payload.json - This allows agents to mount their session-specific subdirectory to access full payloads
- The jq middleware returns: preview (first
PayloadPreviewSizechars, default 500), schema, payloadPath, queryID, originalSize, truncated flag
Understanding the payload.json File:
- The
payload.jsonfile contains the complete original response data in valid JSON format - You can read and parse this file directly using standard JSON parsing tools (e.g.,
cat payload.json | jq .orJSON.parse(fs.readFileSync(path))) - The
payloadSchemain the metadata response shows the structure and types of fields (e.g., "string", "number", "boolean", "array", "object") - The
payloadSchemadoes NOT contain the actual data values - those are only in thepayload.jsonfile - The
payloadPreviewshows the firstPayloadPreviewSizecharacters (default 500) of the JSON for quick reference - To access the full data with all actual values, read the JSON file at
payloadPath
Tools Catalog (tools.json):
- The gateway maintains a catalog of all available tools from backend MCP servers in
tools.json - Located in the log directory (e.g.,
/tmp/gh-aw/mcp-logs/tools.json) - Updated automatically during gateway startup when backend servers are registered
- Format: JSON mapping of server IDs to arrays of tool information
- Each tool includes:
name(tool name without server prefix) anddescription - Example structure:
{ "servers": { "github": [ {"name": "search_code", "description": "Search for code in repositories"}, {"name": "get_file_contents", "description": "Get the contents of a file"} ], "slack": [ {"name": "send_message", "description": "Send a message to a Slack channel"} ] } } - Useful for discovering available tools across all configured backend servers
- Can be used by clients or monitoring tools to understand gateway capabilities
Enhanced Error Context: Command failures include:
- Full command, args, and environment variables
- Context-specific troubleshooting suggestions:
- Docker daemon connectivity checks
- Container image availability
- Network connectivity issues
- MCP protocol compatibility checks
- Auth:
Authorization: <apiKey>header (plain API key per spec 7.1, NOT Bearer scheme) - Sessions: Session ID extracted from Authorization header value
- Stdio servers: Containerized execution only (no direct command support)
- README.md - Full documentation
- MCP Protocol - Specification