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
142 changes: 142 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# MCP Gateway Configuration Example
# This file demonstrates all available configuration options for the MCP Gateway.
# Copy this file to config.toml and customize for your needs.

# ============================================================================
# Gateway Configuration (Optional)
# ============================================================================
# Gateway-level settings that apply to the entire server.

[gateway]
# Port number for the HTTP server (default: 3000)
# 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 = ""

# Domain name for the gateway (optional)
# Used for CORS and other domain-specific features
domain = ""

# Timeout for backend MCP server startup in seconds (default: 60)
# How long to wait for an MCP server to start before timing out
startup_timeout = 60

# Timeout for tool execution in seconds (default: 120)
# How long to wait for a tool call to complete before timing out
tool_timeout = 120

# ============================================================================
# MCP Server Configurations
# ============================================================================
# Define one or more MCP servers that the gateway will proxy to.
# Each server requires a unique name (e.g., [servers.github], [servers.memory])

# Example 1: GitHub MCP Server (stdio via Docker)
[servers.github]
# Required: Docker command to run the container
command = "docker"

# Required: Arguments for the docker command
# Standard pattern: ["run", "--rm", "-i", <env vars>, <container image>]
args = [
"run",
"--rm", # Remove container after exit
"-i", # Interactive mode for stdin/stdout
"-e", "GITHUB_PERSONAL_ACCESS_TOKEN", # Pass through env var from host
"-e", "NO_COLOR=1", # Disable color output
"-e", "TERM=dumb", # Set terminal to dumb mode
"ghcr.io/github/github-mcp-server:latest" # Container image
]

# Optional: Environment variables for the MCP server
# Empty string "" means pass through from host environment
# Non-empty string means set explicit value
[servers.github.env]
GITHUB_PERSONAL_ACCESS_TOKEN = "" # Pass through from host
# DEBUG = "true" # Explicit value example

# Optional: Tool filtering
# tools = ["*"] # Allow all tools (default)
# tools = ["read_file", "list_files"] # Allow specific tools only

# Example 2: Memory MCP Server (stdio via Docker)
[servers.memory]
command = "docker"
args = [
"run",
"--rm",
"-i",
"-e", "NO_COLOR=1",
"-e", "TERM=dumb",
"-e", "PYTHONUNBUFFERED=1", # Python-specific: disable buffering
"mcp/memory"
]

# Example 3: Custom MCP Server with Volume Mounts
[servers.custom]
command = "docker"
args = [
"run",
"--rm",
"-i",
"-v", "${PWD}:/workspace:ro", # Mount current directory (read-only)
"-e", "NO_COLOR=1",
"-e", "TERM=dumb",
"custom/mcp-server:latest"
]

# Example 4: HTTP-based MCP Server (not implemented yet)
# [servers.http-example]
# type = "http"
# url = "https://example.com/mcp"
#
# Optional: HTTP headers for authentication
# [servers.http-example.headers]
# Authorization = "Bearer token123"
# X-API-Key = "api-key-123"

# ============================================================================
# Advanced Options
# ============================================================================

# Enable Data Information Flow Control (DIFC) security model (default: false)
# When true, requires sys___init call before tool access
# This is an experimental feature - keep disabled for standard MCP compatibility
# enable_difc = false

# ============================================================================
# Notes
# ============================================================================
#
# Server Types:
# - "stdio": Process-based MCP server communicating via stdin/stdout (default)
# - "http": HTTP-based MCP server (not yet implemented)
#
# Docker Best Practices:
# - Always use --rm to clean up containers after exit
# - Use -i for interactive mode (required for stdin/stdout communication)
# - Set NO_COLOR=1 and TERM=dumb for better compatibility
# - Use ${VAR_NAME} syntax to reference environment variables
#
# Environment Variables:
# - Empty value ("") passes through from host environment
# - Non-empty value sets explicit value in container
# - Undefined host variables will cause startup errors
#
# Tool Filtering:
# - ["*"] allows all tools (default behavior)
# - ["tool1", "tool2"] allows only specified tools
# - Useful for security and limiting server capabilities
#
# Configuration Validation:
# - Required fields: command, args (for stdio servers)
# - Unknown keys will generate warnings in logs
# - Parse errors will show line numbers for easy debugging
#
# For more information, see:
# - MCP Protocol: https://github.com/modelcontextprotocol
# - Documentation: README.md in this repository
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/githubnext/gh-aw-mcpg
go 1.25.0

require (
github.com/BurntSushi/toml v1.5.0
github.com/BurntSushi/toml v1.6.0
github.com/modelcontextprotocol/go-sdk v1.1.0
github.com/spf13/cobra v1.10.2
golang.org/x/term v0.38.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
37 changes: 25 additions & 12 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,31 @@ var logConfig = logger.New("config:config")
// Config represents the MCPG configuration
type Config struct {
Servers map[string]*ServerConfig `toml:"servers"`
EnableDIFC bool // When true, enables DIFC enforcement and requires sys___init call before tool access. Default is false for standard MCP client compatibility.
Gateway *GatewayConfig // Gateway configuration (port, API key, etc.)
EnableDIFC bool `toml:"enable_difc"` // When true, enables DIFC enforcement and requires sys___init call before tool access. Default is false for standard MCP client compatibility.
Gateway *GatewayConfig `toml:"gateway"` // Gateway configuration (port, API key, etc.)
}

// GatewayConfig represents gateway-level configuration
type GatewayConfig struct {
Port int
APIKey string
Domain string
StartupTimeout int // Seconds
ToolTimeout int // Seconds
Port int `toml:"port"`
APIKey string `toml:"api_key"`
Domain string `toml:"domain"`
StartupTimeout int `toml:"startup_timeout"` // Seconds
ToolTimeout int `toml:"tool_timeout"` // Seconds
}

// ServerConfig represents a single MCP server configuration
type ServerConfig struct {
Type string // "stdio" | "http"
Type string `toml:"type"` // "stdio" | "http"
Command string `toml:"command"`
Args []string `toml:"args"`
Env map[string]string `toml:"env"`
WorkingDirectory string `toml:"working_directory"`
// HTTP-specific fields
URL string // HTTP endpoint URL
Headers map[string]string // HTTP headers for authentication
URL string `toml:"url"` // HTTP endpoint URL
Headers map[string]string `toml:"headers"` // HTTP headers for authentication
// Tool filtering (applies to both stdio and http servers)
Tools []string // Tool filter: ["*"] for all tools, or list of specific tool names
Tools []string `toml:"tools"` // Tool filter: ["*"] for all tools, or list of specific tool names
}

// StdinConfig represents JSON configuration from stdin
Expand Down Expand Up @@ -77,9 +77,22 @@ type StdinGatewayConfig struct {
func LoadFromFile(path string) (*Config, error) {
logConfig.Printf("Loading configuration from file: path=%s", path)
var cfg Config
if _, err := toml.DecodeFile(path, &cfg); err != nil {
meta, err := toml.DecodeFile(path, &cfg)
if err != nil {
// Check if it's a ParseError to provide line numbers
if pErr, ok := err.(toml.ParseError); ok {
return nil, fmt.Errorf("TOML parse error at line %d: %w", pErr.Position.Line, err)
}
return nil, fmt.Errorf("failed to decode TOML: %w", err)
}

// Check for unknown/undecoded keys (typo detection)
if len(meta.Undecoded()) > 0 {
logConfig.Printf("Warning: unknown configuration keys detected: %v", meta.Undecoded())
// For now, just warn - we could make this strict with a flag later
// return nil, fmt.Errorf("unknown configuration keys: %v", meta.Undecoded())
}

logConfig.Printf("Successfully loaded %d servers from TOML file", len(cfg.Servers))
return &cfg, nil
}
Expand Down
158 changes: 158 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -852,3 +853,160 @@ func TestLoadFromStdin_InvalidMountFormat(t *testing.T) {
})
}
}

// Tests for LoadFromFile function with TOML files

func TestLoadFromFile_ValidTOML(t *testing.T) {
// Create a temporary TOML file
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "config.toml")

tomlContent := `
[servers.test]
command = "docker"
args = ["run", "--rm", "-i", "test/container:latest"]

[servers.test.env]
TEST_VAR = "value"
`

err := os.WriteFile(tmpFile, []byte(tomlContent), 0644)
require.NoError(t, err, "Failed to write temp TOML file")

cfg, err := LoadFromFile(tmpFile)
require.NoError(t, err, "LoadFromFile() failed")
require.NotNil(t, cfg, "LoadFromFile() returned nil config")

assert.Len(t, cfg.Servers, 1, "Expected 1 server")
server, ok := cfg.Servers["test"]
require.True(t, ok, "Server 'test' not found")
assert.Equal(t, "docker", server.Command)
assert.Equal(t, []string{"run", "--rm", "-i", "test/container:latest"}, server.Args)
assert.Equal(t, "value", server.Env["TEST_VAR"])
}

func TestLoadFromFile_WithGatewayConfig(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "config.toml")

tomlContent := `
[gateway]
port = 8080
api_key = "test-key-123"
domain = "localhost"
startup_timeout = 30
tool_timeout = 60

[servers.test]
command = "docker"
args = ["run", "--rm", "-i", "test/container:latest"]
`

err := os.WriteFile(tmpFile, []byte(tomlContent), 0644)
require.NoError(t, err, "Failed to write temp TOML file")

cfg, err := LoadFromFile(tmpFile)
require.NoError(t, err, "LoadFromFile() failed")
require.NotNil(t, cfg, "LoadFromFile() returned nil config")
require.NotNil(t, cfg.Gateway, "Gateway config should not be nil")

assert.Equal(t, 8080, cfg.Gateway.Port)
assert.Equal(t, "test-key-123", cfg.Gateway.APIKey)
assert.Equal(t, "localhost", cfg.Gateway.Domain)
assert.Equal(t, 30, cfg.Gateway.StartupTimeout)
assert.Equal(t, 60, cfg.Gateway.ToolTimeout)
}

func TestLoadFromFile_InvalidTOMLWithLineNumber(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "config.toml")

// Invalid TOML: unterminated string on line 2
tomlContent := `[servers.test]
command = "docker
args = ["run"]
`

err := os.WriteFile(tmpFile, []byte(tomlContent), 0644)
require.NoError(t, err, "Failed to write temp TOML file")

cfg, err := LoadFromFile(tmpFile)
require.Error(t, err, "Expected error for invalid TOML")
assert.Nil(t, cfg, "Config should be nil on error")

// Error should contain line number information
assert.Contains(t, err.Error(), "line", "Error should mention line number")
}

func TestLoadFromFile_UnknownKeys(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "config.toml")

// TOML with unknown key "unknown_field"
tomlContent := `
[servers.test]
command = "docker"
args = ["run", "--rm", "-i", "test/container:latest"]
unknown_field = "should trigger warning"
`

err := os.WriteFile(tmpFile, []byte(tomlContent), 0644)
require.NoError(t, err, "Failed to write temp TOML file")

// Should still load successfully but log warning
cfg, err := LoadFromFile(tmpFile)
require.NoError(t, err, "LoadFromFile() should succeed with unknown keys")
require.NotNil(t, cfg, "Config should not be nil")
}

func TestLoadFromFile_NonExistentFile(t *testing.T) {
cfg, err := LoadFromFile("/nonexistent/path/config.toml")
require.Error(t, err, "Expected error for nonexistent file")
assert.Nil(t, cfg, "Config should be nil on error")
}

func TestLoadFromFile_EmptyFile(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "empty.toml")

err := os.WriteFile(tmpFile, []byte(""), 0644)
require.NoError(t, err, "Failed to write temp TOML file")

cfg, err := LoadFromFile(tmpFile)
require.NoError(t, err, "LoadFromFile() should succeed with empty file")
require.NotNil(t, cfg, "Config should not be nil")
assert.Empty(t, cfg.Servers, "Servers should be empty")
}

func TestLoadFromFile_MultipleServers(t *testing.T) {
tmpDir := t.TempDir()
tmpFile := filepath.Join(tmpDir, "config.toml")

tomlContent := `
[servers.github]
command = "docker"
args = ["run", "--rm", "-i", "ghcr.io/github/github-mcp-server:latest"]

[servers.github.env]
GITHUB_TOKEN = ""

[servers.memory]
command = "docker"
args = ["run", "--rm", "-i", "mcp/memory"]
`

err := os.WriteFile(tmpFile, []byte(tomlContent), 0644)
require.NoError(t, err, "Failed to write temp TOML file")

cfg, err := LoadFromFile(tmpFile)
require.NoError(t, err, "LoadFromFile() failed")
require.NotNil(t, cfg, "LoadFromFile() returned nil config")

assert.Len(t, cfg.Servers, 2, "Expected 2 servers")

_, ok := cfg.Servers["github"]
assert.True(t, ok, "Server 'github' not found")

_, ok = cfg.Servers["memory"]
assert.True(t, ok, "Server 'memory' not found")
}