Skip to content

Commit 085c67d

Browse files
committed
fix: support nested arrays wrapped in objects, more elegant implementation
1 parent 5ce9da8 commit 085c67d

File tree

3 files changed

+53
-62
lines changed

3 files changed

+53
-62
lines changed

examples/structured_output/main.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ type UserRequest struct {
3535
UserID string `json:"userId" jsonschema_description:"User ID"`
3636
}
3737

38+
type Asset struct {
39+
ID string `json:"id" jsonschema_description:"Asset identifier"`
40+
Name string `json:"name" jsonschema_description:"Asset name"`
41+
Value float64 `json:"value" jsonschema_description:"Current value"`
42+
Currency string `json:"currency" jsonschema_description:"Currency code"`
43+
}
44+
45+
type AssetListRequest struct {
46+
Limit int `json:"limit,omitempty" jsonschema_description:"Number of assets to return"`
47+
}
48+
3849
func main() {
3950
s := server.NewMCPServer(
4051
"Structured Output Example",
@@ -59,7 +70,15 @@ func main() {
5970
)
6071
s.AddTool(userTool, mcp.NewStructuredToolHandler(getUserProfileHandler))
6172

62-
// Example 3: Manual result creation
73+
// Example 3: Array output - direct array of objects
74+
assetsTool := mcp.NewTool("get_assets",
75+
mcp.WithDescription("Get list of assets as array"),
76+
mcp.WithOutputSchema[[]Asset](),
77+
mcp.WithNumber("limit", mcp.Min(1), mcp.Max(100), mcp.DefaultNumber(10)),
78+
)
79+
s.AddTool(assetsTool, mcp.NewStructuredToolHandler(getAssetsHandler))
80+
81+
// Example 4: Manual result creation
6382
manualTool := mcp.NewTool("manual_structured",
6483
mcp.WithDescription("Manual structured result"),
6584
mcp.WithOutputSchema[WeatherResponse](),
@@ -96,6 +115,27 @@ func getUserProfileHandler(ctx context.Context, request mcp.CallToolRequest, arg
96115
}, nil
97116
}
98117

118+
func getAssetsHandler(ctx context.Context, request mcp.CallToolRequest, args AssetListRequest) ([]Asset, error) {
119+
limit := args.Limit
120+
if limit <= 0 {
121+
limit = 10
122+
}
123+
124+
assets := []Asset{
125+
{ID: "btc", Name: "Bitcoin", Value: 45000.50, Currency: "USD"},
126+
{ID: "eth", Name: "Ethereum", Value: 3200.75, Currency: "USD"},
127+
{ID: "ada", Name: "Cardano", Value: 0.85, Currency: "USD"},
128+
{ID: "sol", Name: "Solana", Value: 125.30, Currency: "USD"},
129+
{ID: "dot", Name: "Pottedot", Value: 18.45, Currency: "USD"},
130+
}
131+
132+
if limit > len(assets) {
133+
limit = len(assets)
134+
}
135+
136+
return assets[:limit], nil
137+
}
138+
99139
func manualWeatherHandler(ctx context.Context, request mcp.CallToolRequest, args WeatherRequest) (*mcp.CallToolResult, error) {
100140
response := WeatherResponse{
101141
Location: args.Location,

mcp/tools.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -635,18 +635,25 @@ func WithOutputSchema[T any]() ToolOption {
635635
var zero T
636636

637637
// Generate schema using invopop/jsonschema library
638-
reflector := jsonschema.Reflector{}
638+
// Configure reflector to generate clean, MCP-compatible schemas
639+
reflector := jsonschema.Reflector{
640+
DoNotReference: true, // Removes $defs map, outputs entire structure inline
641+
Anonymous: true, // Hides auto-generated Schema IDs
642+
AllowAdditionalProperties: true, // Removes additionalProperties: false
643+
}
639644
schema := reflector.Reflect(zero)
640645

641-
// Extract the MCP-compatible schema (inline object schema)
642-
// See how jsonschema library generates the schema: https://github.com/invopop/jsonschema#example
643-
mcpSchema, err := ExtractMCPSchema(schema)
646+
// Clean up schema for MCP compliance
647+
schema.Version = "" // Remove $schema field
648+
649+
// Convert to raw JSON for MCP
650+
mcpSchema, err := json.Marshal(schema)
644651
if err != nil {
645652
// Skip and maintain backward compatibility
646653
return
647654
}
648655

649-
t.RawOutputSchema = mcpSchema
656+
t.RawOutputSchema = json.RawMessage(mcpSchema)
650657
}
651658
}
652659

mcp/utils.go

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@ package mcp
33
import (
44
"encoding/json"
55
"fmt"
6-
"strings"
76

8-
"github.com/invopop/jsonschema"
97
"github.com/spf13/cast"
108
)
119

@@ -479,60 +477,6 @@ func FormatNumberResult(value float64) *CallToolResult {
479477
return NewToolResultText(fmt.Sprintf("%.2f", value))
480478
}
481479

482-
// ExtractMCPSchema converts a full JSON Schema document to the inline format expected by MCP
483-
func ExtractMCPSchema(schema *jsonschema.Schema) (json.RawMessage, error) {
484-
schemaBytes, err := json.Marshal(schema)
485-
if err != nil {
486-
return nil, fmt.Errorf("failed to marshal schema: %w", err)
487-
}
488-
489-
var schemaMap map[string]any
490-
if err := json.Unmarshal(schemaBytes, &schemaMap); err != nil {
491-
return nil, fmt.Errorf("failed to unmarshal schema: %w", err)
492-
}
493-
494-
// Handle $ref case - extract the referenced definition
495-
if ref, hasRef := schemaMap["$ref"].(string); hasRef {
496-
if defs, hasDefs := schemaMap["$defs"].(map[string]any); hasDefs {
497-
// Extract the reference name
498-
refParts := strings.Split(ref, "/")
499-
if len(refParts) > 0 {
500-
defName := refParts[len(refParts)-1]
501-
if defSchema, found := defs[defName].(map[string]any); found {
502-
// Clean up the definition - remove $schema, $id, etc.
503-
cleanSchema := make(map[string]any)
504-
505-
// Copy only type, properties, and required fields
506-
if schemaType, ok := defSchema["type"]; ok {
507-
cleanSchema["type"] = schemaType
508-
}
509-
if properties, ok := defSchema["properties"]; ok {
510-
cleanSchema["properties"] = properties
511-
}
512-
if required, ok := defSchema["required"]; ok {
513-
cleanSchema["required"] = required
514-
}
515-
516-
return json.Marshal(cleanSchema)
517-
}
518-
}
519-
}
520-
}
521-
522-
// If no $ref, clean up the schema directly
523-
cleanSchema := make(map[string]any)
524-
if schemaType, ok := schemaMap["type"]; ok {
525-
cleanSchema["type"] = schemaType
526-
}
527-
if properties, ok := schemaMap["properties"]; ok {
528-
cleanSchema["properties"] = properties
529-
}
530-
if required, ok := schemaMap["required"]; ok {
531-
cleanSchema["required"] = required
532-
}
533-
534-
return json.Marshal(cleanSchema)
535-
}
536480

537481
func ExtractString(data map[string]any, key string) string {
538482
if value, ok := data[key]; ok {

0 commit comments

Comments
 (0)