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
18 changes: 14 additions & 4 deletions internal/config/config_stdin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
package config

import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"os"

"github.com/github/gh-aw-mcpg/internal/logger"
"github.com/santhosh-tekuri/jsonschema/v6"
)

var logStdin = logger.New("config:config_stdin")
Expand Down Expand Up @@ -205,10 +207,18 @@ func (s *StdinServerConfig) UnmarshalJSON(data []byte) error {
return err
}

// Now unmarshal into a map to capture all fields
var allFields map[string]interface{}
if err := json.Unmarshal(data, &allFields); err != nil {
return err
// Now unmarshal into a map to capture all fields.
// Use jsonschema.UnmarshalJSON (which calls decoder.UseNumber()) so that
// numbers are stored as json.Number rather than float64. This preserves
// precision for large integers such as 9007199254740993 that cannot be
// represented exactly as float64.
allFieldsObj, parseErr := jsonschema.UnmarshalJSON(bytes.NewReader(data))
if parseErr != nil {
return parseErr
}
allFields, ok := allFieldsObj.(map[string]interface{})
if !ok {
return fmt.Errorf("expected JSON object for server config, got %T", allFieldsObj)
}

// Known fields in the struct
Expand Down
5 changes: 4 additions & 1 deletion internal/config/config_stdin_unmarshal_coverage_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -154,7 +155,9 @@ func TestStdinServerConfig_UnmarshalJSON_ErrorPaths(t *testing.T) {
err := server.UnmarshalJSON(data)
require.NoError(t, err)
assert.Equal(t, "customValue", server.AdditionalProperties["customField"])
assert.Equal(t, float64(42), server.AdditionalProperties["anotherExtra"])
// Numbers are stored as json.Number (not float64) to preserve precision for
// large integers such as 9007199254740993 that cannot be represented by float64.
assert.Equal(t, json.Number("42"), server.AdditionalProperties["anotherExtra"])
// Known fields must not appear in AdditionalProperties.
_, typeExists := server.AdditionalProperties["type"]
assert.False(t, typeExists, "known field 'type' should not appear in AdditionalProperties")
Expand Down
40 changes: 40 additions & 0 deletions internal/config/validate_server_against_schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,3 +239,43 @@ func TestValidateServerAgainstSchema_TypeMismatchInAdditionalProperties(t *testi
"wrong type for an additional property should cause a schema validation error")
assert.Contains(t, err.Error(), "does not match custom schema")
}

// TestValidateServerAgainstSchema_LargeIntegerPrecision is a regression test that
// verifies large integers (beyond float64 safe range) in additional properties are
// preserved with full precision throughout the validation path.
//
// 9007199254740993 == 2^53+1 cannot be represented exactly as float64
// (it rounds to 9007199254740992). If UnmarshalJSON uses encoding/json the
// const constraint below would fail even though the original JSON is correct.
func TestValidateServerAgainstSchema_LargeIntegerPrecision(t *testing.T) {
const largeInt = `9007199254740993`

// Schema constrains "seq-id" to the exact large-integer value.
schema := compileSchemaForTest(t, `{
"type": "object",
"required": ["seq-id"],
"properties": {
"seq-id": {"const": `+largeInt+`}
}
}`)

// Parse the server config through the real UnmarshalJSON path so that the
// large integer travels through StdinServerConfig.UnmarshalJSON and lands
// in AdditionalProperties.
serverJSON := []byte(`{
"type": "stdio",
"container": "ghcr.io/example/server:latest",
"seq-id": ` + largeInt + `
}`)

var server StdinServerConfig
require.NoError(t, server.UnmarshalJSON(serverJSON))

err := validateServerAgainstSchema(
"test-server", &server, schema,
"https://test.example.com/schema.json",
"mcpServers.test-server",
)
assert.NoError(t, err,
"large integer 9007199254740993 must be preserved without float64 rounding")
}
2 changes: 1 addition & 1 deletion internal/config/validation_schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ func formatSchemaError(err error) error {

AppendConfigDocsFooter(&sb)

return fmt.Errorf("%s", sb.String())
return errors.New(sb.String())
}

return fmt.Errorf("configuration validation error (version: %s): %s", version.Get(), err.Error())
Expand Down
14 changes: 10 additions & 4 deletions internal/config/validation_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ func validateAgainstCustomSchema(name string, server *StdinServerConfig, schemaU
if err != nil {
return schemaErr(
fmt.Sprintf("failed to compile custom schema: %v", err),
fmt.Sprintf("The schema at '%s' must be a valid JSON Schema Draft 7 document", schemaURL))
fmt.Sprintf("The schema at '%s' must be a valid JSON Schema document (Draft 2020-12 or earlier)", schemaURL))
}

logValidation.Printf("Custom schema compiled successfully: name=%s", name)
Expand Down Expand Up @@ -330,12 +330,18 @@ func validateServerAgainstSchema(name string, server *StdinServerConfig, schema
"Internal error - please report this issue")
}

// Unmarshal to map to get struct fields
if err := json.Unmarshal(serverJSON, &serverMap); err != nil {
// Parse using jsonschema.UnmarshalJSON for number-precision consistency with embedded schema path
serverObj, parseErr := jsonschema.UnmarshalJSON(bytes.NewReader(serverJSON))
Comment on lines +333 to +334
if parseErr != nil {
return schemaErr(
fmt.Sprintf("failed to unmarshal server config for validation: %v", err),
fmt.Sprintf("failed to parse server config for validation: %v", parseErr),
"Internal error - please report this issue")
}
if obj, ok := serverObj.(map[string]interface{}); ok {
serverMap = obj
} else {
logValidation.Printf("unexpected: server config parsed to non-object type, using empty map for validation: name=%s", name)
}

// Merge additional properties (custom fields) into the map
for key, value := range server.AdditionalProperties {
Expand Down
Loading