Skip to content

Commit e93a262

Browse files
Claudelpcox
andcommitted
Add payload path prefix configuration to fix underlying path problem
- Added payload_path_prefix field to GatewayConfig - Added --payload-path-prefix flag and MCP_GATEWAY_PAYLOAD_PATH_PREFIX env var - Updated middleware to remap payloadPath using configurable prefix - Updated savePayload() to accept pathPrefix parameter - Updated all test files to include pathPrefix parameter - Added comprehensive documentation in README.md and config examples - All tests pass (make agent-finished successful) Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
1 parent 275e2ad commit e93a262

9 files changed

Lines changed: 102 additions & 32 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,13 @@ See **[Configuration Specification](https://github.com/github/gh-aw/blob/main/do
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`** configures path remapping for clients/agents:
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
@@ -333,6 +340,7 @@ When running locally (`run.sh`), these variables are optional (warnings shown if
333340
| `MCP_GATEWAY_API_KEY` | API authentication key | (disabled) |
334341
| `MCP_GATEWAY_LOG_DIR` | Log file directory (sets default for `--log-dir` flag) | `/tmp/gh-aw/mcp-logs` |
335342
| `MCP_GATEWAY_PAYLOAD_DIR` | Large payload storage directory (sets default for `--payload-dir` flag) | `/tmp/jq-payloads` |
343+
| `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) |
336344
| `MCP_GATEWAY_PAYLOAD_SIZE_THRESHOLD` | Size threshold in bytes for payload storage (sets default for `--payload-size-threshold` flag) | `524288` |
337345
| `DEBUG` | Enable debug logging with pattern matching (e.g., `*`, `server:*,launcher:*`) | (disabled) |
338346
| `DEBUG_COLORS` | Control colored debug output (0 to disable, auto-disabled when piping) | Auto-detect |

config.example-payload-threshold.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,21 @@ 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

internal/cmd/flags_logging.go

Lines changed: 9 additions & 0 deletions
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+
defaultPayloadPathPrefix = "" // Empty by default - use actual filesystem path
1415
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/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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,13 @@ 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.
8390
// Default: 524288 bytes (512KB)

internal/middleware/jqschema.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ func applyJqSchema(ctx context.Context, jsonData interface{}) (interface{}, erro
176176

177177
// savePayload saves the payload to disk and returns the file path
178178
// The file is saved to {baseDir}/{sessionID}/{queryID}/payload.json
179-
func savePayload(baseDir, sessionID, queryID string, payload []byte) (string, error) {
179+
// The returned path uses pathPrefix if provided, otherwise returns the actual filesystem path
180+
func savePayload(baseDir, pathPrefix, sessionID, queryID string, payload []byte) (string, error) {
180181
// Create directory structure: {baseDir}/{sessionID}/{queryID}
181182
dir := filepath.Join(baseDir, sessionID, queryID)
182183

@@ -214,7 +215,19 @@ func savePayload(baseDir, sessionID, queryID string, payload []byte) (string, er
214215
filePath, stat.Size(), stat.Mode())
215216
}
216217

217-
return filePath, nil
218+
// If pathPrefix is provided, use it to remap the path for the client
219+
// This allows the gateway to save files at one path (e.g., /tmp/jq-payloads)
220+
// while returning a different path to clients (e.g., /workspace/payloads)
221+
returnPath := filePath
222+
if pathPrefix != "" {
223+
// Replace baseDir with pathPrefix in the file path
224+
relPath := filepath.Join(sessionID, queryID, "payload.json")
225+
returnPath = filepath.Join(pathPrefix, relPath)
226+
logger.LogInfo("payload", "Remapped payload path for client: filesystem=%s, clientPath=%s, pathPrefix=%s",
227+
filePath, returnPath, pathPrefix)
228+
}
229+
230+
return returnPath, nil
218231
}
219232

220233
// WrapToolHandler wraps a tool handler with jqschema middleware
@@ -224,10 +237,12 @@ func savePayload(baseDir, sessionID, queryID string, payload []byte) (string, er
224237
// 3. If payload size > sizeThreshold: saves to {baseDir}/{sessionID}/{queryID}/payload.json and returns metadata
225238
// 4. If payload size <= sizeThreshold: returns original response directly (no file storage)
226239
// 5. For large payloads: returns first PayloadPreviewSize chars of payload + jq inferred schema
240+
// 6. Uses pathPrefix to remap returned payloadPath for clients (if configured)
227241
func WrapToolHandler(
228242
handler func(context.Context, *sdk.CallToolRequest, interface{}) (*sdk.CallToolResult, interface{}, error),
229243
toolName string,
230244
baseDir string,
245+
pathPrefix string,
231246
sizeThreshold int,
232247
getSessionID func(context.Context) string,
233248
) func(context.Context, *sdk.CallToolRequest, interface{}) (*sdk.CallToolResult, interface{}, error) {
@@ -299,7 +314,7 @@ func WrapToolHandler(
299314
logMiddleware.Printf("Payload exceeds threshold: tool=%s, queryID=%s, size=%d bytes, threshold=%d bytes, saving to disk",
300315
toolName, queryID, payloadSize, sizeThreshold)
301316

302-
filePath, saveErr := savePayload(baseDir, sessionID, queryID, payloadJSON)
317+
filePath, saveErr := savePayload(baseDir, pathPrefix, sessionID, queryID, payloadJSON)
303318
if saveErr != nil {
304319
logMiddleware.Printf("Failed to save payload: tool=%s, queryID=%s, sessionID=%s, error=%v", toolName, queryID, sessionID, saveErr)
305320
logger.LogError("payload", "Failed to save payload to filesystem: tool=%s, queryID=%s, session=%s, error=%v",

internal/middleware/jqschema_integration_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func TestMiddlewareIntegration(t *testing.T) {
6868
}
6969

7070
// Wrap with middleware
71-
wrappedHandler := WrapToolHandler(mockHandler, "github___search_repositories", baseDir, 5, testGetSessionID)
71+
wrappedHandler := WrapToolHandler(mockHandler, "github___search_repositories", baseDir, "", 5, testGetSessionID)
7272

7373
// Call the wrapped handler
7474
result, data, err := wrappedHandler(context.Background(), &sdk.CallToolRequest{}, map[string]interface{}{
@@ -192,7 +192,7 @@ func TestMiddlewareWithLargePayload(t *testing.T) {
192192
}, nil
193193
}
194194

195-
wrappedHandler := WrapToolHandler(mockHandler, "test_tool", baseDir, 5, testGetSessionID)
195+
wrappedHandler := WrapToolHandler(mockHandler, "test_tool", baseDir, "", 5, testGetSessionID)
196196
result, data, err := wrappedHandler(context.Background(), &sdk.CallToolRequest{}, map[string]interface{}{})
197197

198198
require.NoError(t, err)
@@ -248,7 +248,7 @@ func TestMiddlewareDirectoryCreation(t *testing.T) {
248248
return &sdk.CallToolResult{IsError: false}, map[string]interface{}{"test": "data"}, nil
249249
}
250250

251-
wrappedHandler := WrapToolHandler(mockHandler, "test_tool", baseDir, 5, testGetSessionID)
251+
wrappedHandler := WrapToolHandler(mockHandler, "test_tool", baseDir, "", 5, testGetSessionID)
252252
result, data, err := wrappedHandler(context.Background(), &sdk.CallToolRequest{}, map[string]interface{}{})
253253

254254
require.NoError(t, err)

0 commit comments

Comments
 (0)