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
2 changes: 2 additions & 0 deletions .github/agentics/large-payload-tester.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ You are an AI agent trying to verify a secret in a file hosted by the filesystem

Use the filesystem MCP server to access a file called `large-test-file.json`, which contains a field `secret_reference`. Verify that the secret value matches the content of file `secret.txt` which should also be accessed from the filesystem MCP server.

If a payload is too large to return over MCP, the server will return a path in the local filesystem to the payload file instead. Use the path in the local filesystem to access the full payload and extract the secret.

## Important Notes

- **Keep all outputs concise** - Use brief, factual statements
Expand Down
181 changes: 0 additions & 181 deletions .github/workflows/large-payload-tester-README.md

This file was deleted.

69 changes: 39 additions & 30 deletions internal/middleware/jqschema.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ import (

var logMiddleware = logger.New("middleware:jqschema")

// 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 full response can be accessed through the local file system at the payloadPath."

// PayloadMetadata represents the metadata response returned when a payload is too large
// and has been saved to the filesystem
type PayloadMetadata struct {
QueryID string `json:"queryID"`
PayloadPath string `json:"payloadPath"`
Preview string `json:"preview"`
Schema interface{} `json:"schema"`
OriginalSize int `json:"originalSize"`
Truncated bool `json:"truncated"`
Instructions string `json:"instructions"`
}

// jqSchemaFilter is the jq filter that transforms JSON to schema
// This is the same logic as in gh-aw shared/jqschema.md
const jqSchemaFilter = `
Expand Down Expand Up @@ -71,17 +87,18 @@ func generateRandomID() string {
// applyJqSchema applies the jq schema transformation to JSON data
// Uses pre-compiled query code for better performance (3-10x faster than parsing on each request)
// Accepts a context for timeout and cancellation support
func applyJqSchema(ctx context.Context, jsonData interface{}) (string, error) {
// Returns the schema as an interface{} object (not a JSON string)
func applyJqSchema(ctx context.Context, jsonData interface{}) (interface{}, error) {
// Check if compilation succeeded at init time
if jqSchemaCompileErr != nil {
return "", jqSchemaCompileErr
return nil, jqSchemaCompileErr
}

// Run the pre-compiled query with context support (much faster than Parse+Run)
iter := jqSchemaCode.RunWithContext(ctx, jsonData)
v, ok := iter.Next()
if !ok {
return "", fmt.Errorf("jq schema filter returned no results")
return nil, fmt.Errorf("jq schema filter returned no results")
}

// Check for errors with type-specific handling
Expand All @@ -90,22 +107,17 @@ func applyJqSchema(ctx context.Context, jsonData interface{}) (string, error) {
if haltErr, ok := err.(*gojq.HaltError); ok {
// HaltError with nil value means clean halt (not an error)
if haltErr.Value() == nil {
return "", fmt.Errorf("jq schema filter halted cleanly with no output")
return nil, fmt.Errorf("jq schema filter halted cleanly with no output")
}
// HaltError with non-nil value is an actual error
return "", fmt.Errorf("jq schema filter halted with error (exit code %d): %w", haltErr.ExitCode(), err)
return nil, fmt.Errorf("jq schema filter halted with error (exit code %d): %w", haltErr.ExitCode(), err)
}
// Generic error case
return "", fmt.Errorf("jq schema filter error: %w", err)
}

// Convert result to JSON
schemaJSON, err := json.Marshal(v)
if err != nil {
return "", fmt.Errorf("failed to marshal schema result: %w", err)
return nil, fmt.Errorf("jq schema filter error: %w", err)
}

return string(schemaJSON), nil
// Return the schema object directly (no JSON marshaling needed here)
return v, nil
}

// savePayload saves the payload to disk and returns the file path
Expand Down Expand Up @@ -234,7 +246,7 @@ func WrapToolHandler(

// Apply jq schema transformation
logger.LogDebug("payload", "Applying jq schema transformation: tool=%s, queryID=%s", toolName, queryID)
var schemaJSON string
var schemaObj interface{}
if schemaErr := func() error {
// Unmarshal to interface{} for jq processing
var jsonData interface{}
Expand All @@ -246,7 +258,7 @@ func WrapToolHandler(
if err != nil {
return err
}
schemaJSON = schema
schemaObj = schema
return nil
}(); schemaErr != nil {
logMiddleware.Printf("Failed to apply jq schema: tool=%s, queryID=%s, sessionID=%s, error=%v", toolName, queryID, sessionID, schemaErr)
Expand All @@ -256,8 +268,10 @@ func WrapToolHandler(
return result, data, err
}

// Calculate schema size for logging (marshal temporarily)
schemaBytes, _ := json.Marshal(schemaObj)
logger.LogDebug("payload", "Schema transformation completed: tool=%s, queryID=%s, schemaSize=%d bytes",
toolName, queryID, len(schemaJSON))
toolName, queryID, len(schemaBytes))
Comment on lines +271 to +274
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

The error from json.Marshal(schemaObj) is ignored. If marshaling ever fails, this will log a schemaSize of 0 without any signal. Please handle the error (at least log it) or avoid marshaling just to compute size.

See below for a potential fix:

		schemaBytes, err := json.Marshal(schemaObj)
		if err != nil {
			logMiddleware.Printf("Failed to marshal schema for size calculation: tool=%s, queryID=%s, sessionID=%s, error=%v", toolName, queryID, sessionID, err)
			logger.LogWarn("payload", "Failed to marshal schema for size calculation: tool=%s, queryID=%s, error=%v",
				toolName, queryID, err)
		} else {
			logger.LogDebug("payload", "Schema transformation completed: tool=%s, queryID=%s, schemaSize=%d bytes",
				toolName, queryID, len(schemaBytes))
		}

Copilot uses AI. Check for mistakes.

// Build the transformed response: first 500 chars + schema
payloadStr := string(payloadJSON)
Expand All @@ -273,27 +287,22 @@ func WrapToolHandler(
toolName, queryID, len(payloadStr))
}

// Create rewritten response
rewrittenResponse := map[string]interface{}{
"queryID": queryID,
"payloadPath": filePath,
"preview": preview,
"schema": schemaJSON,
"originalSize": len(payloadJSON),
"truncated": truncated,
// Create rewritten response using the PayloadMetadata struct
rewrittenResponse := PayloadMetadata{
QueryID: queryID,
PayloadPath: filePath,
Preview: preview,
Comment on lines +292 to +294
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

PayloadPath is populated from filePath even when savePayload fails (in that case filePath is ""). Since the client only receives the rewritten metadata response, this can drop the actual tool payload and return an unusable/empty path. Consider short-circuiting: if filePath is empty (or saveErr != nil), return the original result/data instead of transforming the response, or include an explicit error and avoid rewriting.

Copilot uses AI. Check for mistakes.
Schema: schemaObj,
OriginalSize: len(payloadJSON),
Truncated: truncated,
Instructions: PayloadTruncatedInstructions,
}
Comment on lines +297 to 299
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

PayloadTruncatedInstructions says the payload was “too large”, but Instructions is set unconditionally even when Truncated is false. This makes the metadata response misleading for small payloads. Either only set Instructions when truncated is true, or adjust the message/field name so it’s accurate for both truncated and non-truncated cases.

Copilot uses AI. Check for mistakes.

logMiddleware.Printf("Rewritten response: tool=%s, queryID=%s, sessionID=%s, originalSize=%d, truncated=%v",
toolName, queryID, sessionID, len(payloadJSON), truncated)
logger.LogInfo("payload", "Created metadata response for client: tool=%s, queryID=%s, session=%s, payloadPath=%s, originalSize=%d bytes, truncated=%v",
toolName, queryID, sessionID, filePath, len(payloadJSON), truncated)

// Parse the schema JSON string back to an object for cleaner display
var schemaObj interface{}
if err := json.Unmarshal([]byte(schemaJSON), &schemaObj); err == nil {
rewrittenResponse["schema"] = schemaObj
}

// Marshal the rewritten response to JSON for the Content field
rewrittenJSON, marshalErr := json.Marshal(rewrittenResponse)
if marshalErr != nil {
Expand Down
24 changes: 19 additions & 5 deletions internal/middleware/jqschema_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ import (
"github.com/stretchr/testify/require"
)

// integrationPayloadMetadataToMap converts PayloadMetadata to map[string]interface{} for test assertions
func integrationPayloadMetadataToMap(t *testing.T, data interface{}) map[string]interface{} {
t.Helper()
pm, ok := data.(PayloadMetadata)
if !ok {
t.Fatalf("expected PayloadMetadata, got %T", data)
}
jsonBytes, err := json.Marshal(pm)
require.NoError(t, err)
var result map[string]interface{}
err = json.Unmarshal(jsonBytes, &result)
require.NoError(t, err)
return result
Comment on lines +15 to +27
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

This helper duplicates payloadMetadataToMap from jqschema_test.go with only a name change. Consider moving a single shared helper into a common *_test.go file in this package and using it from both unit and integration tests to reduce duplication and keep assertions consistent.

Suggested change
// integrationPayloadMetadataToMap converts PayloadMetadata to map[string]interface{} for test assertions
func integrationPayloadMetadataToMap(t *testing.T, data interface{}) map[string]interface{} {
t.Helper()
pm, ok := data.(PayloadMetadata)
if !ok {
t.Fatalf("expected PayloadMetadata, got %T", data)
}
jsonBytes, err := json.Marshal(pm)
require.NoError(t, err)
var result map[string]interface{}
err = json.Unmarshal(jsonBytes, &result)
require.NoError(t, err)
return result
// Ensure encoding/json import is used even though this file delegates
// metadata-to-map conversion to a shared test helper.
var _ = json.Marshal
// integrationPayloadMetadataToMap converts PayloadMetadata to map[string]interface{} for test assertions
func integrationPayloadMetadataToMap(t *testing.T, data interface{}) map[string]interface{} {
t.Helper()
return payloadMetadataToMap(t, data)

Copilot uses AI. Check for mistakes.
}

// TestMiddlewareIntegration tests the complete middleware flow
func TestMiddlewareIntegration(t *testing.T) {
// Create temporary directory for test
Expand Down Expand Up @@ -88,8 +103,7 @@ func TestMiddlewareIntegration(t *testing.T) {
assert.Len(t, queryIDFromContent, 32, "QueryID should be 32 hex characters")

// Verify response structure in data return value (for internal use)
dataMap, ok := data.(map[string]interface{})
require.True(t, ok, "Response should be a map")
dataMap := integrationPayloadMetadataToMap(t, data)

// Check all required fields exist
assert.Contains(t, dataMap, "queryID")
Expand Down Expand Up @@ -170,7 +184,7 @@ func TestMiddlewareIntegration(t *testing.T) {
assert.False(t, dataMap["truncated"].(bool), "Should not be truncated for small payloads")

// Verify originalSize
originalSize := dataMap["originalSize"].(int)
originalSize := int(dataMap["originalSize"].(float64))
assert.Greater(t, originalSize, 0, "Original size should be positive")
}

Expand Down Expand Up @@ -222,7 +236,7 @@ func TestMiddlewareWithLargePayload(t *testing.T) {
}

// Also check data return value
dataMap := data.(map[string]interface{})
dataMap := integrationPayloadMetadataToMap(t, data)

// Verify truncation occurred
truncated := dataMap["truncated"].(bool)
Expand Down Expand Up @@ -277,7 +291,7 @@ func TestMiddlewareDirectoryCreation(t *testing.T) {
queryIDFromContent := contentMap["queryID"].(string)

// Also check data return value
dataMap := data.(map[string]interface{})
dataMap := integrationPayloadMetadataToMap(t, data)
queryID := dataMap["queryID"].(string)

// Both should match
Expand Down
Loading
Loading