Skip to content

Commit f61e2d9

Browse files
Copilotpelikhan
andcommitted
Add documentation and integration test for mcp-gateway
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent daf61c0 commit f61e2d9

2 files changed

Lines changed: 182 additions & 0 deletions

File tree

docs/mcp-gateway.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# MCP Gateway Command
2+
3+
The `gh aw mcp-gateway` command implements a minimal MCP proxy application that aggregates multiple MCP servers into a single HTTP gateway.
4+
5+
## Features
6+
7+
- **Integrates with sandbox.mcp**: Works with the `sandbox.mcp` extension point in workflows
8+
- **Multiple MCP servers**: Supports connecting to multiple MCP servers simultaneously
9+
- **MCP protocol support**: Implements `initialize`, `list_tools`, `call_tool`, `list_resources`, `list_prompts`
10+
- **Transport support**: Currently supports stdio/command transport, HTTP transport planned
11+
- **Comprehensive logging**: Logs to file in MCP log directory (`/tmp/gh-aw/mcp-gateway-logs` by default)
12+
- **API key authentication**: Optional API key for securing gateway endpoints
13+
14+
## Usage
15+
16+
### Basic Usage
17+
18+
```bash
19+
# From stdin (reads JSON config from standard input)
20+
echo '{"mcpServers":{"gh-aw":{"command":"gh","args":["aw","mcp-server"]}}}' | gh aw mcp-gateway
21+
22+
# From config file
23+
gh aw mcp-gateway --config config.json
24+
25+
# Custom port and log directory
26+
gh aw mcp-gateway --config config.json --port 8088 --log-dir /custom/logs
27+
```
28+
29+
### Configuration Format
30+
31+
The gateway accepts configuration in JSON format:
32+
33+
```json
34+
{
35+
"mcpServers": {
36+
"server-name": {
37+
"command": "command-to-run",
38+
"args": ["arg1", "arg2"],
39+
"env": {
40+
"ENV_VAR": "value"
41+
}
42+
},
43+
"http-server": {
44+
"url": "http://localhost:3000"
45+
}
46+
},
47+
"gateway": {
48+
"port": 8080,
49+
"apiKey": "optional-api-key"
50+
}
51+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
//go:build integration
2+
3+
package cli
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"net/http"
9+
"os"
10+
"path/filepath"
11+
"testing"
12+
"time"
13+
)
14+
15+
func TestMCPGateway_BasicStartup(t *testing.T) {
16+
// Skip if the binary doesn't exist
17+
binaryPath := "../../gh-aw"
18+
if _, err := os.Stat(binaryPath); os.IsNotExist(err) {
19+
t.Skip("Skipping test: gh-aw binary not found. Run 'make build' first.")
20+
}
21+
22+
// Create temporary config
23+
tmpDir := t.TempDir()
24+
configFile := filepath.Join(tmpDir, "gateway-config.json")
25+
26+
config := MCPGatewayConfig{
27+
MCPServers: map[string]MCPServerConfig{
28+
"gh-aw": {
29+
Command: binaryPath,
30+
Args: []string{"mcp-server"},
31+
},
32+
},
33+
Gateway: GatewaySettings{
34+
Port: 8088,
35+
},
36+
}
37+
38+
configJSON, err := json.Marshal(config)
39+
if err != nil {
40+
t.Fatalf("Failed to marshal config: %v", err)
41+
}
42+
43+
if err := os.WriteFile(configFile, configJSON, 0644); err != nil {
44+
t.Fatalf("Failed to write config file: %v", err)
45+
}
46+
47+
// Start gateway in background
48+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
49+
defer cancel()
50+
51+
// Use the runMCPGateway function directly in a goroutine
52+
errChan := make(chan error, 1)
53+
go func() {
54+
errChan <- runMCPGateway(configFile, 8088, tmpDir)
55+
}()
56+
57+
// Wait for server to start
58+
select {
59+
case <-ctx.Done():
60+
t.Fatal("Context canceled before server could start")
61+
case <-time.After(2 * time.Second):
62+
// Server should be ready
63+
}
64+
65+
// Test health endpoint
66+
resp, err := http.Get("http://localhost:8088/health")
67+
if err != nil {
68+
cancel()
69+
t.Fatalf("Failed to connect to gateway: %v", err)
70+
}
71+
defer resp.Body.Close()
72+
73+
if resp.StatusCode != http.StatusOK {
74+
t.Errorf("Expected status 200, got %d", resp.StatusCode)
75+
}
76+
77+
// Test servers list endpoint
78+
resp, err = http.Get("http://localhost:8088/servers")
79+
if err != nil {
80+
cancel()
81+
t.Fatalf("Failed to get servers list: %v", err)
82+
}
83+
defer resp.Body.Close()
84+
85+
var serversResp map[string]any
86+
if err := json.NewDecoder(resp.Body).Decode(&serversResp); err != nil {
87+
t.Fatalf("Failed to decode servers response: %v", err)
88+
}
89+
90+
servers, ok := serversResp["servers"].([]any)
91+
if !ok {
92+
t.Fatal("Expected servers array in response")
93+
}
94+
95+
if len(servers) != 1 {
96+
t.Errorf("Expected 1 server, got %d", len(servers))
97+
}
98+
99+
// Check if gh-aw server is present
100+
foundGhAw := false
101+
for _, server := range servers {
102+
if serverName, ok := server.(string); ok && serverName == "gh-aw" {
103+
foundGhAw = true
104+
break
105+
}
106+
}
107+
108+
if !foundGhAw {
109+
t.Error("Expected gh-aw server in servers list")
110+
}
111+
112+
// Cancel context to stop the server
113+
cancel()
114+
115+
// Wait for server to stop or timeout
116+
select {
117+
case err := <-errChan:
118+
// Server stopped, check if it was a clean shutdown
119+
if err != nil && err != http.ErrServerClosed && err.Error() != "context canceled" {
120+
t.Logf("Server stopped with error: %v", err)
121+
}
122+
case <-time.After(2 * time.Second):
123+
t.Log("Server shutdown timed out")
124+
}
125+
}
126+
127+
func TestMCPGateway_ConfigFromStdin(t *testing.T) {
128+
// This test would require piping config to stdin
129+
// which is more complex in Go tests, so we'll skip for now
130+
t.Skip("Stdin config test requires more complex setup")
131+
}

0 commit comments

Comments
 (0)