Skip to content

Commit 670a95a

Browse files
authored
fix: handle either $defs or definitions field when unmarshaling ToolArgumentsSchema (#645)
Older versions of the JSON Schema spec use the "definitions" field, while newer versions use "$defs". This can be seen in the Honeycomb MCP, which uses "definitions". This commit adds support for both fields, and prefers "$defs" over "definitions" if both are present.
1 parent 6bd3269 commit 670a95a

File tree

2 files changed

+158
-0
lines changed

2 files changed

+158
-0
lines changed

mcp/tools.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,31 @@ func (tis ToolArgumentsSchema) MarshalJSON() ([]byte, error) {
653653
return json.Marshal(m)
654654
}
655655

656+
// UnmarshalJSON implements the json.Unmarshaler interface for ToolArgumentsSchema.
657+
// It handles both "$defs" (JSON Schema 2019-09+) and "definitions" (JSON Schema draft-07)
658+
// by reading either field and storing it in the Defs field.
659+
func (tis *ToolArgumentsSchema) UnmarshalJSON(data []byte) error {
660+
// Use a temporary type to avoid infinite recursion
661+
type Alias ToolArgumentsSchema
662+
aux := &struct {
663+
Definitions map[string]any `json:"definitions,omitempty"`
664+
*Alias
665+
}{
666+
Alias: (*Alias)(tis),
667+
}
668+
669+
if err := json.Unmarshal(data, aux); err != nil {
670+
return err
671+
}
672+
673+
// If $defs wasn't provided but definitions was, use definitions
674+
if tis.Defs == nil && aux.Definitions != nil {
675+
tis.Defs = aux.Definitions
676+
}
677+
678+
return nil
679+
}
680+
656681
type ToolAnnotation struct {
657682
// Human-readable title for the tool
658683
Title string `json:"title,omitempty"`

mcp/tools_test.go

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1549,3 +1549,136 @@ func TestToolMetaMarshalingOmitsWhenNil(t *testing.T) {
15491549
// Check that _meta field is not present
15501550
assert.NotContains(t, result, "_meta", "Tool without Meta should not include _meta field")
15511551
}
1552+
1553+
func TestToolArgumentsSchema_UnmarshalWithDefinitions(t *testing.T) {
1554+
// Test that "definitions" (JSON Schema draft-07) is properly unmarshaled into Defs field
1555+
jsonData := `{
1556+
"type": "object",
1557+
"properties": {
1558+
"operation": {
1559+
"$ref": "#/definitions/operation_type"
1560+
}
1561+
},
1562+
"required": ["operation"],
1563+
"definitions": {
1564+
"operation_type": {
1565+
"type": "string",
1566+
"enum": ["create", "read", "update", "delete"]
1567+
}
1568+
}
1569+
}`
1570+
1571+
var schema ToolArgumentsSchema
1572+
err := json.Unmarshal([]byte(jsonData), &schema)
1573+
assert.NoError(t, err)
1574+
1575+
// Verify the schema was properly unmarshaled
1576+
assert.Equal(t, "object", schema.Type)
1577+
assert.Contains(t, schema.Properties, "operation")
1578+
assert.Equal(t, []string{"operation"}, schema.Required)
1579+
1580+
// Most importantly: verify that "definitions" was read into Defs field
1581+
assert.NotNil(t, schema.Defs)
1582+
assert.Contains(t, schema.Defs, "operation_type")
1583+
1584+
operationType, ok := schema.Defs["operation_type"].(map[string]any)
1585+
assert.True(t, ok)
1586+
assert.Equal(t, "string", operationType["type"])
1587+
assert.NotNil(t, operationType["enum"])
1588+
}
1589+
1590+
func TestToolArgumentsSchema_UnmarshalWithDefs(t *testing.T) {
1591+
// Test that "$defs" (JSON Schema 2019-09+) is properly unmarshaled into Defs field
1592+
jsonData := `{
1593+
"type": "object",
1594+
"properties": {
1595+
"operation": {
1596+
"$ref": "#/$defs/operation_type"
1597+
}
1598+
},
1599+
"required": ["operation"],
1600+
"$defs": {
1601+
"operation_type": {
1602+
"type": "string",
1603+
"enum": ["create", "read", "update", "delete"]
1604+
}
1605+
}
1606+
}`
1607+
1608+
var schema ToolArgumentsSchema
1609+
err := json.Unmarshal([]byte(jsonData), &schema)
1610+
assert.NoError(t, err)
1611+
1612+
// Verify the schema was properly unmarshaled
1613+
assert.Equal(t, "object", schema.Type)
1614+
assert.Contains(t, schema.Properties, "operation")
1615+
assert.Equal(t, []string{"operation"}, schema.Required)
1616+
1617+
// Verify that "$defs" was read into Defs field
1618+
assert.NotNil(t, schema.Defs)
1619+
assert.Contains(t, schema.Defs, "operation_type")
1620+
1621+
operationType, ok := schema.Defs["operation_type"].(map[string]any)
1622+
assert.True(t, ok)
1623+
assert.Equal(t, "string", operationType["type"])
1624+
assert.NotNil(t, operationType["enum"])
1625+
}
1626+
1627+
func TestToolArgumentsSchema_UnmarshalPrefersDefs(t *testing.T) {
1628+
// Test that if both "$defs" and "definitions" are present, "$defs" takes precedence
1629+
jsonData := `{
1630+
"type": "object",
1631+
"$defs": {
1632+
"from_defs": {
1633+
"type": "string"
1634+
}
1635+
},
1636+
"definitions": {
1637+
"from_definitions": {
1638+
"type": "integer"
1639+
}
1640+
}
1641+
}`
1642+
1643+
var schema ToolArgumentsSchema
1644+
err := json.Unmarshal([]byte(jsonData), &schema)
1645+
assert.NoError(t, err)
1646+
1647+
// $defs should take precedence
1648+
assert.Contains(t, schema.Defs, "from_defs")
1649+
assert.NotContains(t, schema.Defs, "from_definitions")
1650+
}
1651+
1652+
func TestToolArgumentsSchema_MarshalRoundTrip(t *testing.T) {
1653+
// Test that marshaling and unmarshaling preserves definitions
1654+
original := ToolArgumentsSchema{
1655+
Type: "object",
1656+
Properties: map[string]any{
1657+
"field": map[string]any{
1658+
"$ref": "#/$defs/my_type",
1659+
},
1660+
},
1661+
Required: []string{"field"},
1662+
Defs: map[string]any{
1663+
"my_type": map[string]any{
1664+
"type": "string",
1665+
"enum": []string{"a", "b", "c"},
1666+
},
1667+
},
1668+
}
1669+
1670+
// Marshal
1671+
data, err := json.Marshal(original)
1672+
assert.NoError(t, err)
1673+
1674+
// Unmarshal
1675+
var unmarshaled ToolArgumentsSchema
1676+
err = json.Unmarshal(data, &unmarshaled)
1677+
assert.NoError(t, err)
1678+
1679+
// Verify round-trip
1680+
assert.Equal(t, original.Type, unmarshaled.Type)
1681+
assert.Equal(t, original.Required, unmarshaled.Required)
1682+
assert.NotNil(t, unmarshaled.Defs)
1683+
assert.Contains(t, unmarshaled.Defs, "my_type")
1684+
}

0 commit comments

Comments
 (0)