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
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,14 +398,14 @@ DEBUG_COLORS=0 DEBUG=* ./awmg --config config.toml
- Default payload directory: `/tmp/jq-payloads` (configurable via `--payload-dir` flag, `MCP_GATEWAY_PAYLOAD_DIR` env var, or `payload_dir` in 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 500 chars), schema, payloadPath, queryID, originalSize, truncated flag
- The jq middleware returns: preview (first `PayloadPreviewSize` chars, default 500), schema, payloadPath, queryID, originalSize, truncated flag

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This documentation refers to PayloadPreviewSize as "chars". The code currently truncates previews by bytes (and slices strings by byte index), so this should be updated to "bytes" or the implementation should be changed to be rune/character-based.

This issue also appears on line 408 of the same file.

See below for a potential fix:

- The jq middleware returns: preview (first `PayloadPreviewSize` bytes, default 500), schema, payloadPath, queryID, originalSize, truncated flag

**Understanding the payload.json File:**
- The `payload.json` file 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 .` or `JSON.parse(fs.readFileSync(path))`)
- The `payloadSchema` in the metadata response shows the **structure and types** of fields (e.g., "string", "number", "boolean", "array", "object")
- The `payloadSchema` does NOT contain the actual data values - those are only in the `payload.json` file
- The `payloadPreview` shows the first `PayloadPreviewSize` bytes (default 500) of the JSON for quick reference

Copilot uses AI. Check for mistakes.

**Understanding the payload.json File:**
- The `payload.json` file 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 .` or `JSON.parse(fs.readFileSync(path))`)
- The `payloadSchema` in the metadata response shows the **structure and types** of fields (e.g., "string", "number", "boolean", "array", "object")
- The `payloadSchema` does NOT contain the actual data values - those are only in the `payload.json` file
- The `payloadPreview` shows the first 500 characters of the JSON for quick reference
- The `payloadPreview` shows the first `PayloadPreviewSize` characters (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):**
Expand Down
8 changes: 4 additions & 4 deletions internal/middleware/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This middleware package implements the jqschema functionality from the gh-aw sha
- **Automatic JSON Schema Inference**: Uses the jq schema transformation logic to automatically infer the structure and types of JSON responses
- **Payload Storage**: Stores complete response payloads in `/tmp/gh-awmg/tools-calls/{randomID}/payload.json`
- **Response Rewriting**: Returns a transformed response containing:
- First 500 characters of the payload (for quick preview)
- First `PayloadPreviewSize` (500) characters of the payload (for quick preview)

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs describe the preview limit as "characters" and also hardcode "(500)" next to PayloadPreviewSize. Since the implementation currently truncates by bytes, and the constant value may change, consider wording like "first PayloadPreviewSize bytes (default 500)" and avoid repeating the numeric value to prevent docs drifting out of sync.

This issue also appears in the following locations of the same file:

  • line 74
  • line 133
Suggested change
- First `PayloadPreviewSize` (500) characters of the payload (for quick preview)
- First `PayloadPreviewSize` bytes of the payload (default 500, for quick preview)

Copilot uses AI. Check for mistakes.
- Inferred JSON schema showing structure and types
- Query ID for tracking
- File path to complete payload
Expand Down Expand Up @@ -71,7 +71,7 @@ The middleware is automatically applied to all backend MCP server tools (except
**Understanding the response:**
- `payloadPath`: Points to a JSON file containing the **complete original response data**
- `payloadSchema`: Shows the **structure and types** (e.g., "string", "number", "boolean") but NOT the actual values
- `payloadPreview`: First 500 characters of the JSON for quick reference
- `payloadPreview`: First `PayloadPreviewSize` (500) characters of the JSON for quick reference
- `originalSize`: Size of the full response in bytes

**Reading the payload.json file:**
Expand Down Expand Up @@ -130,15 +130,15 @@ func ShouldApplyMiddleware(toolName string) bool {
**Selective Middleware Mounting**: A configuration system could be added to:
- Enable/disable middleware per backend server
- Configure which tools get middleware applied
- Set custom truncation limits
- Set custom truncation limits (currently `PayloadPreviewSize = 500`)
- Configure storage locations
- Add multiple middleware types with ordering

Example future config structure:
```toml
[middleware.jqschema]
enabled = true
truncate_at = 500
preview_size = 500
storage_path = "/tmp/gh-awmg/tools-calls"
exclude_tools = ["sys___*"]
include_backends = ["github", "tavily"]
Expand Down
16 changes: 10 additions & 6 deletions internal/middleware/jqschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ var logMiddleware = logger.New("middleware:jqschema")
// This prevents malformed queries or large payloads from causing hangs
const DefaultJqTimeout = 5 * time.Second

// PayloadPreviewSize is the maximum number of characters to include in the payload preview
// This controls how much of the original payload is returned inline when a payload is stored to disk
Comment on lines +25 to +26

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PayloadPreviewSize is documented as a character count, but the implementation uses len(payloadStr) and payloadStr[:PayloadPreviewSize], which operate on bytes and can also split multi-byte UTF-8 sequences. Either (a) clarify that this is a byte limit (update the comment/name), or (b) switch truncation to be rune-aware so the docs match behavior and previews don’t cut in the middle of a UTF-8 rune.

This issue also appears on line 344 of the same file.

Suggested change
// PayloadPreviewSize is the maximum number of characters to include in the payload preview
// This controls how much of the original payload is returned inline when a payload is stored to disk
// PayloadPreviewSize is the maximum number of bytes to include in the payload preview
// This controls how much of the original payload (in UTF-8 bytes) is returned inline when a payload is stored to disk

Copilot uses AI. Check for mistakes.
const PayloadPreviewSize = 500

// PayloadTruncatedInstructions is the message returned to clients when a payload
// has been truncated and saved to the filesystem
const PayloadTruncatedInstructions = "The payload was too large for an MCP response. The complete original response data is saved as a JSON file at payloadPath. The file contains valid JSON that can be parsed directly. The payloadSchema shows the structure and types of fields in the full response, but not the actual values. To access the full data with all values, read and parse the JSON file at payloadPath."
Expand Down Expand Up @@ -219,7 +223,7 @@ func savePayload(baseDir, sessionID, queryID string, payload []byte) (string, er
// 2. Extracts session ID from context (or uses "default")
// 3. If payload size > sizeThreshold: saves to {baseDir}/{sessionID}/{queryID}/payload.json and returns metadata
// 4. If payload size <= sizeThreshold: returns original response directly (no file storage)
// 5. For large payloads: returns first 500 chars of payload + jq inferred schema
// 5. For large payloads: returns first PayloadPreviewSize chars of payload + jq inferred schema
func WrapToolHandler(
handler func(context.Context, *sdk.CallToolRequest, interface{}) (*sdk.CallToolResult, interface{}, error),
toolName string,
Expand Down Expand Up @@ -337,14 +341,14 @@ func WrapToolHandler(
logger.LogDebug("payload", "Schema transformation completed: tool=%s, queryID=%s, schemaSize=%d bytes",
toolName, queryID, len(schemaBytes))

// Build the transformed response: first 500 chars + schema
// Build the transformed response: first PayloadPreviewSize chars + schema
payloadStr := string(payloadJSON)
var preview string
truncated := len(payloadStr) > 500
truncated := len(payloadStr) > PayloadPreviewSize
if truncated {
preview = payloadStr[:500] + "..."
logger.LogInfo("payload", "Payload truncated for preview: tool=%s, queryID=%s, originalSize=%d bytes, previewSize=500 bytes",
toolName, queryID, len(payloadStr))
preview = payloadStr[:PayloadPreviewSize] + "..."
logger.LogInfo("payload", "Payload truncated for preview: tool=%s, queryID=%s, originalSize=%d bytes, previewSize=%d bytes",
toolName, queryID, len(payloadStr), PayloadPreviewSize)
} else {
preview = payloadStr
logger.LogDebug("payload", "Payload small enough for full preview: tool=%s, queryID=%s, size=%d bytes",
Expand Down
4 changes: 2 additions & 2 deletions internal/middleware/jqschema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func TestWrapToolHandler_LongPayload(t *testing.T) {

// Verify preview truncation in Content field
preview := contentMap["payloadPreview"].(string)
assert.LessOrEqual(t, len(preview), 503, "Preview should be truncated to ~500 chars + '...'")
assert.LessOrEqual(t, len(preview), PayloadPreviewSize+3, "Preview should be truncated to PayloadPreviewSize chars + '...'")

Copilot AI Feb 19, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion and its message talk about "chars", but len(preview) is a byte count in Go. To avoid confusion (and potential issues if payloads contain non-ASCII), either update the wording to "bytes" or change the truncation logic/tests to be rune-aware.

Suggested change
assert.LessOrEqual(t, len(preview), PayloadPreviewSize+3, "Preview should be truncated to PayloadPreviewSize chars + '...'")
assert.LessOrEqual(t, len(preview), PayloadPreviewSize+3, "Preview should be truncated to PayloadPreviewSize bytes + '...'")

Copilot uses AI. Check for mistakes.
assert.True(t, strings.HasSuffix(preview, "..."), "Preview should end with '...'")
}

Expand Down Expand Up @@ -321,7 +321,7 @@ func TestPayloadStorage_SessionIsolation(t *testing.T) {
func TestPayloadStorage_LargePayloadPreserved(t *testing.T) {
baseDir := t.TempDir()

// Create a large payload (> 500 chars to trigger truncation)
// Create a large payload (> PayloadPreviewSize chars to trigger truncation)
largeContent := strings.Repeat("This is a large payload content. ", 100) // ~3400 chars
largePayload := map[string]interface{}{
"total_count": 1000,
Expand Down