From 1d331fb5371d899674c62fa8033718ac4645e1d3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:11:44 +0000 Subject: [PATCH 1/2] Initial plan From e95f024944b282df77aa6eb2361cba1b39597a99 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Feb 2026 00:14:42 +0000 Subject: [PATCH 2/2] Centralize payload preview size to PayloadPreviewSize constant Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com> --- AGENTS.md | 4 ++-- internal/middleware/README.md | 8 ++++---- internal/middleware/jqschema.go | 16 ++++++++++------ internal/middleware/jqschema_test.go | 4 ++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 3df71f51b..50639145d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 **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):** diff --git a/internal/middleware/README.md b/internal/middleware/README.md index 2bb00cf1a..72eac895f 100644 --- a/internal/middleware/README.md +++ b/internal/middleware/README.md @@ -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) - Inferred JSON schema showing structure and types - Query ID for tracking - File path to complete payload @@ -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:** @@ -130,7 +130,7 @@ 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 @@ -138,7 +138,7 @@ 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"] diff --git a/internal/middleware/jqschema.go b/internal/middleware/jqschema.go index edf1f10ff..1aabcf796 100644 --- a/internal/middleware/jqschema.go +++ b/internal/middleware/jqschema.go @@ -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 +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." @@ -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, @@ -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", diff --git a/internal/middleware/jqschema_test.go b/internal/middleware/jqschema_test.go index 8578b5ac2..479a6cbbf 100644 --- a/internal/middleware/jqschema_test.go +++ b/internal/middleware/jqschema_test.go @@ -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 + '...'") assert.True(t, strings.HasSuffix(preview, "..."), "Preview should end with '...'") } @@ -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,