Skip to content

Commit a29c937

Browse files
authored
Addressing PR comments (#1441)
2 parents e8079cd + 0203bba commit a29c937

12 files changed

Lines changed: 347 additions & 52 deletions

ISSUE_18295_FIX.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
# Fix for GitHub Issue #18295: MCP Tool Calling Loop Issues
2+
3+
## Problem Statement
4+
5+
Agents were getting stuck in infinite loops when calling MCP tools like `github-list_commits`. The agent would repeatedly make the same tool call, receive a response indicating the payload was too large, attempt to read the payload file, fail (FILE_NOT_FOUND), and retry - creating an infinite loop that would only stop when the workflow timed out.
6+
7+
## Root Cause Analysis
8+
9+
The issue was caused by the interaction between three factors:
10+
11+
1. **Low Default Threshold**: The default payload size threshold was set to 10KB (10,240 bytes)
12+
2. **Typical GitHub API Response Sizes**: GitHub API responses (especially `list_commits` over 3 days) frequently exceed 10KB:
13+
- Small query (1-5 commits): ~2-5KB
14+
- Medium query (10-30 commits): **10-50KB** ← Often exceeds threshold
15+
- Large query (100+ commits): 100KB-1MB
16+
17+
3. **Inaccessible Payload Path**: When a response exceeded 10KB:
18+
- The middleware would save the payload to disk at an **absolute host path**: `/tmp/jq-payloads/{sessionID}/{queryID}/payload.json`
19+
- The middleware would return metadata with `payloadPath` pointing to this host path
20+
- The agent would try to read this path, but it **doesn't exist in the agent's container filesystem**
21+
- The agent would see `FILE_NOT_FOUND` and retry the tool call
22+
- This created an infinite loop
23+
24+
## Example from Issue #18295
25+
26+
From the user's logs:
27+
```
28+
github-list_commits
29+
└ {"agentInstructions":"The payload was too large for an MCP response. The comp...
30+
31+
✗ bash: cat /tmp/gh-aw/mcp-payloads/***/a47e03f1b3561c858a06b84d5e02eb38/payload.json 2>/dev/null || echo "FILE_NOT_FOUND"
32+
"description": Required
33+
```
34+
35+
The agent received:
36+
- `agentInstructions`: "The payload was too large..."
37+
- `payloadPath`: `/tmp/jq-payloads/{session}/{query}/payload.json`
38+
39+
Then tried to read the file, got `FILE_NOT_FOUND`, and retried the tool call.
40+
41+
## Solution
42+
43+
**Increase the default payload size threshold from 10KB to 512KB (524,288 bytes)**
44+
45+
This ensures that:
46+
1. Typical GitHub API responses (10-50KB) are returned **inline** without disk storage
47+
2. Only truly large responses (>512KB) trigger the payload-to-disk mechanism
48+
3. Agents don't encounter the inaccessible file path issue for normal operations
49+
4. The threshold can still be overridden via:
50+
- Environment variable: `MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD=<bytes>`
51+
- Command-line flag: `--payload-size-threshold <bytes>`
52+
- Config file: `payload_size_threshold = <bytes>`
53+
54+
## Changes Made
55+
56+
### Code Changes
57+
58+
1. **internal/config/config_payload.go**
59+
- Changed `DefaultPayloadSizeThreshold` from `10240` to `524288`
60+
- Updated comment to explain rationale
61+
62+
2. **internal/cmd/flags_logging.go**
63+
- Changed `defaultPayloadSizeThreshold` from `10240` to `524288`
64+
- Updated comment
65+
66+
3. **internal/config/config_core.go**
67+
- Updated comment from "10KB" to "512KB"
68+
69+
4. **internal/cmd/flags_logging_test.go**
70+
- Updated test assertion from `10240` to `524288`
71+
72+
### Documentation Changes
73+
74+
1. **README.md**
75+
- Updated CLI flag default from `10240` to `524288`
76+
- Updated environment variable table default from `10240` to `524288`
77+
- Updated configuration alternative default from `10240` to `524288`
78+
79+
2. **config.example-payload-threshold.toml**
80+
- Updated default from `10240` to `524288`
81+
- Updated examples to use larger, more realistic values:
82+
- 256KB (more aggressive storage)
83+
- 512KB (default)
84+
- 1MB (minimize disk storage)
85+
86+
## Testing
87+
88+
All tests pass with the new default:
89+
- Unit tests: ✅ PASS (all packages)
90+
- Integration tests: ✅ PASS
91+
- Configuration tests: ✅ PASS
92+
93+
## Impact
94+
95+
### Before Fix (10KB threshold)
96+
- GitHub `list_commits` responses frequently exceeded threshold
97+
- Agents got stuck in infinite loops trying to read inaccessible files
98+
- Workflows would timeout after repeatedly calling the same tool
99+
- Poor user experience
100+
101+
### After Fix (512KB threshold)
102+
- GitHub `list_commits` responses are returned inline (typical size 10-50KB)
103+
- Agents receive complete data without file system access issues
104+
- No more infinite loops for typical use cases
105+
- Greatly improved user experience
106+
107+
### Performance Considerations
108+
109+
**Memory Impact**: Minimal. Most responses are still well under 512KB.
110+
111+
**Network Impact**: Reduced. Returning data inline is faster than writing to disk and returning metadata.
112+
113+
**Disk I/O Impact**: Significantly reduced. Fewer responses trigger disk storage.
114+
115+
## Configuration Options
116+
117+
Users can still customize the threshold for their specific needs:
118+
119+
```bash
120+
# Lower threshold (more aggressive disk storage)
121+
MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD=262144 ./awmg --config config.toml
122+
123+
# Higher threshold (minimize disk storage)
124+
MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD=1048576 ./awmg --config config.toml
125+
126+
# Or via config file
127+
[gateway]
128+
payload_size_threshold = 524288
129+
```
130+
131+
## Related Issue
132+
133+
GitHub Issue: https://github.com/github/gh-aw/issues/18295
134+
135+
## Backward Compatibility
136+
137+
**Fully backward compatible**
138+
139+
- Existing configurations continue to work
140+
- Environment variable and CLI flag still functional
141+
- Users can explicitly set the old 10KB threshold if desired
142+
- New default is a **quality-of-life improvement** that makes the gateway work better out-of-the-box
143+
144+
## Future Considerations
145+
146+
If payload path accessibility issues persist for responses >512KB:
147+
148+
1. Consider adding a mount configuration to make payload paths accessible to agents
149+
2. Consider adding a flag to return large payloads inline (disable disk storage)
150+
3. Consider implementing payload compression to reduce size before threshold check
151+
4. Consider per-tool threshold configuration for tools known to return large responses
152+
153+
## Summary
154+
155+
The fix addresses the root cause of the infinite loop issue by ensuring that typical MCP tool responses are small enough to be returned inline, avoiding the inaccessible file path problem entirely. The threshold remains configurable for advanced use cases, maintaining flexibility while providing sensible defaults.

README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -220,11 +220,18 @@ See **[Configuration Specification](https://github.com/github/gh-aw/blob/main/do
220220

221221
**Configuration Alternatives**:
222222
- **`payloadSizeThreshold`** is not supported in JSON stdin format. Use:
223-
- CLI flag: `--payload-size-threshold <bytes>` (default: 10240)
223+
- CLI flag: `--payload-size-threshold <bytes>` (default: 524288)
224224
- Environment variable: `MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD=<bytes>`
225225
- TOML config file: `payload_size_threshold = <bytes>` in `[gateway]` section
226226
- Payloads **larger** than this threshold are stored to disk and return metadata
227227
- Payloads **smaller than or equal** to this threshold are returned inline
228+
- **`payloadPathPrefix`** is not supported in JSON stdin format. Use:
229+
- CLI flag: `--payload-path-prefix <path>` (default: empty - use actual filesystem path)
230+
- Environment variable: `MCP_GATEWAY_PAYLOAD_PATH_PREFIX=<path>`
231+
- TOML config file: `payload_path_prefix = "<path>"` in `[gateway]` section
232+
- When set, the `payloadPath` returned to clients uses this prefix instead of the actual filesystem path
233+
- Example: Gateway saves to `/tmp/jq-payloads/session/query/payload.json`, but returns `/workspace/payloads/session/query/payload.json` to clients if `payload_path_prefix = "/workspace/payloads"`
234+
- This allows agents running in containers to access payload files via mounted volumes
228235

229236
**Environment Variable Features**:
230237
- **Passthrough**: Set value to empty string (`""`) to pass through from host
@@ -297,13 +304,14 @@ Flags:
297304
-l, --listen string HTTP server listen address (default "127.0.0.1:3000")
298305
--log-dir string Directory for log files (falls back to stdout if directory cannot be created) (default "/tmp/gh-aw/mcp-logs")
299306
--payload-dir string Directory for storing large payload files (segmented by session ID) (default "/tmp/jq-payloads")
300-
--payload-size-threshold int Size threshold (in bytes) for storing payloads to disk. Payloads larger than this are stored, smaller ones returned inline (default 10240)
307+
--payload-path-prefix string Path prefix to use when returning payloadPath to clients (allows remapping host paths to client/agent container paths)
308+
--payload-size-threshold int Size threshold (in bytes) for storing payloads to disk. Payloads larger than this are stored, smaller ones returned inline (default 524288)
301309
--routed Run in routed mode (each backend at /mcp/<server>)
302-
--sequential-launch Launch MCP servers sequentially during startup (parallel launch is default)
303-
--unified Run in unified mode (all backends at /mcp)
304-
--validate-env Validate execution environment (Docker, env vars) before starting
305-
-v, --verbose count Increase verbosity level (use -v for info, -vv for debug, -vvv for trace)
306-
--version version for awmg
310+
--sequential-launch Launch MCP servers sequentially during startup (parallel launch is default)
311+
--unified Run in unified mode (all backends at /mcp)
312+
--validate-env Validate execution environment (Docker, env vars) before starting
313+
-v, --verbose count Increase verbosity level (use -v for info, -vv for debug, -vvv for trace)
314+
--version version for awmg
307315
308316
Use "awmg [command] --help" for more information about a command.
309317
```
@@ -333,7 +341,8 @@ When running locally (`run.sh`), these variables are optional (warnings shown if
333341
| `MCP_GATEWAY_API_KEY` | API authentication key | (disabled) |
334342
| `MCP_GATEWAY_LOG_DIR` | Log file directory (sets default for `--log-dir` flag) | `/tmp/gh-aw/mcp-logs` |
335343
| `MCP_GATEWAY_PAYLOAD_DIR` | Large payload storage directory (sets default for `--payload-dir` flag) | `/tmp/jq-payloads` |
336-
| `MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD` | Size threshold in bytes for payload storage (sets default for `--payload-size-threshold` flag) | `10240` |
344+
| `MCP_GATEWAY_PAYLOAD_PATH_PREFIX` | Path prefix for remapping payloadPath returned to clients (sets default for `--payload-path-prefix` flag) | (empty - use actual filesystem path) |
345+
| `MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD` | Size threshold in bytes for payload storage (sets default for `--payload-size-threshold` flag) | `524288` |
337346
| `DEBUG` | Enable debug logging with pattern matching (e.g., `*`, `server:*,launcher:*`) | (disabled) |
338347
| `DEBUG_COLORS` | Control colored debug output (0 to disable, auto-disabled when piping) | Auto-detect |
339348

config.example-payload-threshold.toml

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# 1. Command-line flag: --payload-size-threshold 2048
66
# 2. Environment variable: MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD=2048
77
# 3. Config file: payload_size_threshold = 2048
8-
# 4. Default: 10240 bytes (10KB)
8+
# 4. Default: 524288 bytes (512KB)
99

1010
[gateway]
1111
port = 3000
@@ -18,21 +18,35 @@ api_key = "your-api-key-here"
1818
# Default: /tmp/jq-payloads
1919
payload_dir = "/tmp/jq-payloads"
2020

21+
# Payload path prefix for remapping file paths returned to clients
22+
# When set, payloadPath uses this prefix instead of the actual filesystem path
23+
# This allows agents in containers to access payload files via mounted volumes
24+
#
25+
# Can also be set via:
26+
# - Flag: --payload-path-prefix /workspace/payloads
27+
# - Env: MCP_GATEWAY_PAYLOAD_PATH_PREFIX=/workspace/payloads
28+
# Default: (empty - use actual filesystem path)
29+
#
30+
# Example:
31+
# Gateway saves to: /tmp/jq-payloads/session123/query456/payload.json
32+
# Returns to client: /workspace/payloads/session123/query456/payload.json
33+
# Agent mounts: -v /tmp/jq-payloads:/workspace/payloads
34+
# payload_path_prefix = "/workspace/payloads"
35+
2136
# Payload size threshold (in bytes) for storing responses to disk
2237
# Payloads LARGER than this threshold are stored to disk and return metadata
2338
# Payloads SMALLER than or equal to this threshold are returned inline
2439
#
2540
# Can also be set via:
2641
# - Flag: --payload-size-threshold 2048
2742
# - Env: MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD=2048
28-
# Default: 10240 bytes (10KB)
43+
# Default: 524288 bytes (512KB)
2944
#
3045
# Examples:
31-
# payload_size_threshold = 512 # 512 bytes - more aggressive file storage
32-
# payload_size_threshold = 1024 # 1KB - more aggressive file storage
33-
# payload_size_threshold = 2048 # 2KB - fewer files, more inline responses
34-
# payload_size_threshold = 10240 # 10KB - default, good for most use cases
35-
payload_size_threshold = 10240
46+
# payload_size_threshold = 262144 # 256KB - more aggressive file storage
47+
# payload_size_threshold = 524288 # 512KB - default, balances inline vs disk storage
48+
# payload_size_threshold = 1048576 # 1MB - minimize disk storage for most use cases
49+
payload_size_threshold = 524288
3650

3751
[servers.github]
3852
command = "docker"

internal/cmd/flags_logging.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,23 @@ import (
1111
const (
1212
defaultLogDir = "/tmp/gh-aw/mcp-logs"
1313
defaultPayloadDir = "/tmp/jq-payloads"
14-
defaultPayloadSizeThreshold = 10240 // 10KB default threshold
14+
defaultPayloadPathPrefix = "" // Empty by default - use actual filesystem path
15+
defaultPayloadSizeThreshold = 524288 // 512KB default threshold
1516
)
1617

1718
// Logging flag variables
1819
var (
1920
logDir string
2021
payloadDir string
22+
payloadPathPrefix string
2123
payloadSizeThreshold int
2224
)
2325

2426
func init() {
2527
RegisterFlag(func(cmd *cobra.Command) {
2628
cmd.Flags().StringVar(&logDir, "log-dir", getDefaultLogDir(), "Directory for log files (falls back to stdout if directory cannot be created)")
2729
cmd.Flags().StringVar(&payloadDir, "payload-dir", getDefaultPayloadDir(), "Directory for storing large payload files (segmented by session ID)")
30+
cmd.Flags().StringVar(&payloadPathPrefix, "payload-path-prefix", getDefaultPayloadPathPrefix(), "Path prefix to use when returning payloadPath to clients (allows remapping host paths to client/agent container paths)")
2831
cmd.Flags().IntVar(&payloadSizeThreshold, "payload-size-threshold", getDefaultPayloadSizeThreshold(), "Size threshold (in bytes) for storing payloads to disk. Payloads larger than this are stored, smaller ones returned inline")
2932
})
3033
}
@@ -41,6 +44,12 @@ func getDefaultPayloadDir() string {
4144
return envutil.GetEnvString("MCP_GATEWAY_PAYLOAD_DIR", defaultPayloadDir)
4245
}
4346

47+
// getDefaultPayloadPathPrefix returns the default payload path prefix, checking MCP_GATEWAY_PAYLOAD_PATH_PREFIX
48+
// environment variable first, then falling back to the hardcoded default
49+
func getDefaultPayloadPathPrefix() string {
50+
return envutil.GetEnvString("MCP_GATEWAY_PAYLOAD_PATH_PREFIX", defaultPayloadPathPrefix)
51+
}
52+
4453
// getDefaultPayloadSizeThreshold returns the default payload size threshold, checking
4554
// MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD environment variable first, then falling back to the hardcoded default
4655
func getDefaultPayloadSizeThreshold() int {

internal/cmd/flags_logging_test.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ func TestPayloadSizeThresholdFlagDefault(t *testing.T) {
154154
t.Setenv("MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD", "")
155155

156156
result := getDefaultPayloadSizeThreshold()
157-
assert.Equal(t, 10240, result, "Default should be 10240 bytes")
157+
assert.Equal(t, 524288, result, "Default should be 524288 bytes (512KB)")
158158
}
159159

160160
func TestPayloadSizeThresholdEnvVar(t *testing.T) {
@@ -163,3 +163,43 @@ func TestPayloadSizeThresholdEnvVar(t *testing.T) {
163163
result := getDefaultPayloadSizeThreshold()
164164
assert.Equal(t, 4096, result, "Environment variable should override default")
165165
}
166+
167+
func TestGetDefaultPayloadPathPrefix(t *testing.T) {
168+
tests := []struct {
169+
name string
170+
envValue string
171+
setEnv bool
172+
expected string
173+
}{
174+
{
175+
name: "no env var - returns default",
176+
setEnv: false,
177+
expected: defaultPayloadPathPrefix,
178+
},
179+
{
180+
name: "env var set - returns custom path",
181+
envValue: "/workspace/payloads",
182+
setEnv: true,
183+
expected: "/workspace/payloads",
184+
},
185+
{
186+
name: "empty env var - returns default",
187+
envValue: "",
188+
setEnv: true,
189+
expected: defaultPayloadPathPrefix,
190+
},
191+
}
192+
193+
for _, tt := range tests {
194+
t.Run(tt.name, func(t *testing.T) {
195+
if tt.setEnv {
196+
t.Setenv("MCP_GATEWAY_PAYLOAD_PATH_PREFIX", tt.envValue)
197+
} else {
198+
t.Setenv("MCP_GATEWAY_PAYLOAD_PATH_PREFIX", "")
199+
}
200+
201+
result := getDefaultPayloadPathPrefix()
202+
assert.Equal(t, tt.expected, result)
203+
})
204+
}
205+
}

internal/cmd/root.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,14 @@ func run(cmd *cobra.Command, args []string) error {
225225
cfg.Gateway.PayloadDir = payloadDir
226226
}
227227

228+
// Apply payload path prefix flag (if different from default, it was explicitly set)
229+
if cmd.Flags().Changed("payload-path-prefix") {
230+
cfg.Gateway.PayloadPathPrefix = payloadPathPrefix
231+
} else if payloadPathPrefix != "" && payloadPathPrefix != defaultPayloadPathPrefix {
232+
// Environment variable was set
233+
cfg.Gateway.PayloadPathPrefix = payloadPathPrefix
234+
}
235+
228236
// Apply payload size threshold flag (if different from default, it was explicitly set)
229237
if cmd.Flags().Changed("payload-size-threshold") {
230238
cfg.Gateway.PayloadSizeThreshold = payloadSizeThreshold

internal/config/config_core.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,16 @@ type GatewayConfig struct {
7878
// PayloadDir is the directory for storing large payloads
7979
PayloadDir string `toml:"payload_dir" json:"payload_dir,omitempty"`
8080

81+
// PayloadPathPrefix is the path prefix to use when returning payloadPath to clients.
82+
// This allows remapping the host filesystem path to a path accessible in the client/agent container.
83+
// If empty, the actual filesystem path (PayloadDir) is returned.
84+
// Example: If PayloadDir="/tmp/jq-payloads" and PayloadPathPrefix="/workspace/payloads",
85+
// then payloadPath will be "/workspace/payloads/{sessionID}/{queryID}/payload.json"
86+
PayloadPathPrefix string `toml:"payload_path_prefix" json:"payload_path_prefix,omitempty"`
87+
8188
// PayloadSizeThreshold is the size threshold (in bytes) for storing payloads to disk.
8289
// Payloads larger than this threshold are stored to disk, smaller ones are returned inline.
83-
// Default: 10240 bytes (10KB)
90+
// Default: 524288 bytes (512KB)
8491
PayloadSizeThreshold int `toml:"payload_size_threshold" json:"payload_size_threshold,omitempty"`
8592
}
8693

internal/config/config_payload.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ const DefaultPayloadDir = "/tmp/jq-payloads"
77

88
// DefaultPayloadSizeThreshold is the default size threshold (in bytes) for storing payloads to disk.
99
// Payloads larger than this threshold are stored to disk, smaller ones are returned inline.
10-
// Default: 10240 bytes (10KB)
11-
const DefaultPayloadSizeThreshold = 10240
10+
// Default: 524288 bytes (512KB) - chosen to accommodate typical MCP tool responses including
11+
// GitHub API queries (list_commits, list_issues, etc.) without triggering disk storage.
12+
// This prevents agent looping issues when payloadPath is not accessible in agent containers.
13+
const DefaultPayloadSizeThreshold = 524288
1214

1315
func init() {
1416
// Register default setter for PayloadDir and PayloadSizeThreshold

0 commit comments

Comments
 (0)