diff --git a/examples/structured_input_and_output/main.go b/examples/structured_input_and_output/main.go index f932def08..467157222 100644 --- a/examples/structured_input_and_output/main.go +++ b/examples/structured_input_and_output/main.go @@ -9,41 +9,41 @@ import ( "github.com/mark3labs/mcp-go/server" ) -// Note: The jsonschema_description tag is added to the JSON schema as description +// Note: The jsonschema tag is added to the JSON schema as description // Ideally use better descriptions, this is just an example type WeatherRequest struct { - Location string `json:"location" jsonschema_description:"City or location" jsonschema:"required"` - Units string `json:"units,omitempty" jsonschema_description:"celsius or fahrenheit" jsonschema:"enum=celsius,enum=fahrenheit"` + Location string `json:"location" jsonschema:"City or location"` + Units string `json:"units,omitempty" jsonschema:"celsius or fahrenheit"` } type WeatherResponse struct { - Location string `json:"location" jsonschema_description:"Location"` - Temperature float64 `json:"temperature" jsonschema_description:"Temperature"` - Units string `json:"units" jsonschema_description:"Units"` - Conditions string `json:"conditions" jsonschema_description:"Weather conditions"` - Timestamp time.Time `json:"timestamp" jsonschema_description:"When retrieved"` + Location string `json:"location" jsonschema:"Location"` + Temperature float64 `json:"temperature" jsonschema:"Temperature"` + Units string `json:"units" jsonschema:"Units"` + Conditions string `json:"conditions" jsonschema:"Weather conditions"` + Timestamp time.Time `json:"timestamp" jsonschema:"When retrieved"` } type UserProfile struct { - ID string `json:"id" jsonschema_description:"User ID"` - Name string `json:"name" jsonschema_description:"Full name"` - Email string `json:"email" jsonschema_description:"Email"` - Tags []string `json:"tags" jsonschema_description:"User tags"` + ID string `json:"id" jsonschema:"User ID"` + Name string `json:"name" jsonschema:"Full name"` + Email string `json:"email" jsonschema:"Email"` + Tags []string `json:"tags" jsonschema:"User tags"` } type UserRequest struct { - UserID string `json:"userId" jsonschema_description:"User ID" jsonschema:"required"` + UserID string `json:"userId" jsonschema:"User ID"` } type Asset struct { - ID string `json:"id" jsonschema_description:"Asset identifier"` - Name string `json:"name" jsonschema_description:"Asset name"` - Value float64 `json:"value" jsonschema_description:"Current value"` - Currency string `json:"currency" jsonschema_description:"Currency code"` + ID string `json:"id" jsonschema:"Asset identifier"` + Name string `json:"name" jsonschema:"Asset name"` + Value float64 `json:"value" jsonschema:"Current value"` + Currency string `json:"currency" jsonschema:"Currency code"` } type AssetListRequest struct { - Limit int `json:"limit,omitempty" jsonschema_description:"Number of assets to return" jsonschema:"minimum=1,maximum=100,default=10"` + Limit int `json:"limit,omitempty" jsonschema:"Number of assets to return"` } func main() { diff --git a/go.mod b/go.mod index 23d69d6fc..2e3e292d0 100644 --- a/go.mod +++ b/go.mod @@ -3,19 +3,15 @@ module github.com/mark3labs/mcp-go go 1.23.0 require ( + github.com/google/jsonschema-go v0.4.2 github.com/google/uuid v1.6.0 - github.com/invopop/jsonschema v0.13.0 github.com/spf13/cast v1.7.1 github.com/stretchr/testify v1.9.0 github.com/yosida95/uritemplate/v3 v3.0.2 ) require ( - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 70e9c33da..1f7b85acf 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,17 @@ -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= +github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= @@ -27,8 +20,6 @@ github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/mcp/tools.go b/mcp/tools.go index f0de72582..0ed3cf9eb 100644 --- a/mcp/tools.go +++ b/mcp/tools.go @@ -8,7 +8,7 @@ import ( "reflect" "strconv" - "github.com/invopop/jsonschema" + "github.com/google/jsonschema-go/jsonschema" ) var errToolSchemaConflict = errors.New("provide either InputSchema or RawInputSchema, not both") @@ -843,24 +843,13 @@ func WithDeferLoading(deferLoading bool) ToolOption { // It accepts any Go type, usually a struct, and automatically generates a JSON schema from it. func WithInputSchema[T any]() ToolOption { return func(t *Tool) { - var zero T - - // Generate schema using invopop/jsonschema library - // Configure reflector to generate clean, MCP-compatible schemas - reflector := jsonschema.Reflector{ - DoNotReference: true, // Removes $defs map, outputs entire structure inline - Anonymous: true, // Hides auto-generated Schema IDs - AllowAdditionalProperties: true, // Removes additionalProperties: false + schema, err := jsonschema.For[T](&jsonschema.ForOptions{IgnoreInvalidTypes: true}) + if err != nil { + return } - schema := reflector.Reflect(zero) - - // Clean up schema for MCP compliance - schema.Version = "" // Remove $schema field - // Convert to raw JSON for MCP mcpSchema, err := json.Marshal(schema) if err != nil { - // Skip and maintain backward compatibility return } @@ -904,30 +893,17 @@ func WithRawInputSchema(schema json.RawMessage) ToolOption { // It accepts any Go type, usually a struct, and automatically generates a JSON schema from it. func WithOutputSchema[T any]() ToolOption { return func(t *Tool) { - var zero T - - // Generate schema using invopop/jsonschema library - // Configure reflector to generate clean, MCP-compatible schemas - reflector := jsonschema.Reflector{ - DoNotReference: true, // Removes $defs map, outputs entire structure inline - Anonymous: true, // Hides auto-generated Schema IDs - AllowAdditionalProperties: true, // Removes additionalProperties: false + schema, err := jsonschema.For[T](&jsonschema.ForOptions{IgnoreInvalidTypes: true}) + if err != nil { + return } - schema := reflector.Reflect(zero) - - // Clean up schema for MCP compliance - schema.Version = "" // Remove $schema field - // Convert to raw JSON for MCP mcpSchema, err := json.Marshal(schema) if err != nil { - // Skip and maintain backward compatibility return } - // Retrieve the schema from raw JSON if err := json.Unmarshal(mcpSchema, &t.OutputSchema); err != nil { - // Skip and maintain backward compatibility return } diff --git a/mcp/tools_test.go b/mcp/tools_test.go index 5788a20e4..e190c7389 100644 --- a/mcp/tools_test.go +++ b/mcp/tools_test.go @@ -635,9 +635,9 @@ func TestFlexibleArgumentsJSONMarshalUnmarshal(t *testing.T) { // generates an MCP-compatible JSON output schema for a tool func TestToolWithInputSchema(t *testing.T) { type TestInput struct { - Name string `json:"name" jsonschema_description:"Person's name" jsonschema:"required"` - Age int `json:"age" jsonschema_description:"Person's age"` - Email string `json:"email,omitempty" jsonschema_description:"Email address" jsonschema:"required"` + Name string `json:"name" jsonschema:"Person's name"` + Age int `json:"age" jsonschema:"Person's age"` + Email string `json:"email,omitempty" jsonschema:"Email address"` } tool := NewTool("test_tool", @@ -678,15 +678,41 @@ func TestToolWithInputSchema(t *testing.T) { assert.Contains(t, propertiesMap, "name") assert.Contains(t, propertiesMap, "age") assert.Contains(t, propertiesMap, "email") + + // Verify schema type is "object" + assert.Equal(t, "object", schemaMap["type"]) + + // Verify required field contents: name and age required, email (omitempty) not required + requiredSlice, ok := requiredList.([]any) + assert.True(t, ok) + assert.Contains(t, requiredSlice, "name") + assert.Contains(t, requiredSlice, "age") + assert.NotContains(t, requiredSlice, "email") + + // Verify field descriptions match jsonschema tag values + nameProp, ok := propertiesMap["name"].(map[string]any) + assert.True(t, ok) + assert.Equal(t, "Person's name", nameProp["description"]) + + ageProp, ok := propertiesMap["age"].(map[string]any) + assert.True(t, ok) + assert.Equal(t, "Person's age", ageProp["description"]) + + emailProp, ok := propertiesMap["email"].(map[string]any) + assert.True(t, ok) + assert.Equal(t, "Email address", emailProp["description"]) + + // Verify additionalProperties is false + assert.Equal(t, false, schemaMap["additionalProperties"]) } // TestToolWithOutputSchema tests that the WithOutputSchema function // generates an MCP-compatible JSON output schema for a tool func TestToolWithOutputSchema(t *testing.T) { type TestOutput struct { - Name string `json:"name" jsonschema_description:"Person's name"` - Age int `json:"age" jsonschema_description:"Person's age"` - Email string `json:"email,omitempty" jsonschema_description:"Email address"` + Name string `json:"name" jsonschema:"Person's name"` + Age int `json:"age" jsonschema:"Person's age"` + Email string `json:"email,omitempty" jsonschema:"Email address"` } tests := []struct { @@ -728,6 +754,42 @@ func TestToolWithOutputSchema(t *testing.T) { if tt.expectedOutputSchema { assert.True(t, exists) assert.NotNil(t, outputSchema) + + outputSchemaMap, ok := outputSchema.(map[string]any) + assert.True(t, ok) + + // Verify schema type is "object" + assert.Equal(t, "object", outputSchemaMap["type"]) + + // Verify properties exist + outputProps, ok := outputSchemaMap["properties"].(map[string]any) + assert.True(t, ok) + assert.Contains(t, outputProps, "name") + assert.Contains(t, outputProps, "age") + assert.Contains(t, outputProps, "email") + + // Verify required field contents: name and age required, email (omitempty) not required + outputRequired, ok := outputSchemaMap["required"].([]any) + assert.True(t, ok) + assert.Contains(t, outputRequired, "name") + assert.Contains(t, outputRequired, "age") + assert.NotContains(t, outputRequired, "email") + + // Verify field descriptions match jsonschema tag values + nameProp, ok := outputProps["name"].(map[string]any) + assert.True(t, ok) + assert.Equal(t, "Person's name", nameProp["description"]) + + ageProp, ok := outputProps["age"].(map[string]any) + assert.True(t, ok) + assert.Equal(t, "Person's age", ageProp["description"]) + + emailProp, ok := outputProps["email"].(map[string]any) + assert.True(t, ok) + assert.Equal(t, "Email address", emailProp["description"]) + + // Verify additionalProperties is false + assert.Equal(t, false, outputSchemaMap["additionalProperties"]) } else { assert.False(t, exists) assert.Nil(t, outputSchema) diff --git a/www/docs/pages/servers/tools.mdx b/www/docs/pages/servers/tools.mdx index f8b59717c..80373b0cc 100644 --- a/www/docs/pages/servers/tools.mdx +++ b/www/docs/pages/servers/tools.mdx @@ -130,10 +130,10 @@ Define your input parameters as a Go struct and use `WithInputSchema`: ```go // Define input struct with JSON schema tags type SearchRequest struct { - Query string `json:"query" jsonschema_description:"Search query" jsonschema:"required"` - Limit int `json:"limit,omitempty" jsonschema_description:"Maximum results" jsonschema:"minimum=1,maximum=100,default=10"` - Categories []string `json:"categories,omitempty" jsonschema_description:"Filter by categories"` - SortBy string `json:"sortBy,omitempty" jsonschema_description:"Sort field" jsonschema:"enum=relevance,enum=date,enum=popularity"` + Query string `json:"query" jsonschema:"Search query"` + Limit int `json:"limit,omitempty" jsonschema:"Maximum results"` + Categories []string `json:"categories,omitempty" jsonschema:"Filter by categories"` + SortBy string `json:"sortBy,omitempty" jsonschema:"Sort field"` } // Create tool with struct-based input schema @@ -150,17 +150,17 @@ Define structured output for predictable tool responses: ```go // Define output struct type SearchResponse struct { - Query string `json:"query" jsonschema_description:"Original search query"` - TotalCount int `json:"totalCount" jsonschema_description:"Total matching products"` - Products []Product `json:"products" jsonschema_description:"Search results"` - ProcessedAt time.Time `json:"processedAt" jsonschema_description:"When search was performed"` + Query string `json:"query" jsonschema:"Original search query"` + TotalCount int `json:"totalCount" jsonschema:"Total matching products"` + Products []Product `json:"products" jsonschema:"Search results"` + ProcessedAt time.Time `json:"processedAt" jsonschema:"When search was performed"` } type Product struct { - ID string `json:"id" jsonschema_description:"Product ID"` - Name string `json:"name" jsonschema_description:"Product name"` - Price float64 `json:"price" jsonschema_description:"Price in USD"` - InStock bool `json:"inStock" jsonschema_description:"Availability"` + ID string `json:"id" jsonschema:"Product ID"` + Name string `json:"name" jsonschema:"Product name"` + Price float64 `json:"price" jsonschema:"Price in USD"` + InStock bool `json:"inStock" jsonschema:"Availability"` } // Create tool with both input and output schemas @@ -222,17 +222,17 @@ Tools can return arrays of structured data: ```go // Define asset struct type Asset struct { - ID string `json:"id" jsonschema_description:"Asset identifier"` - Name string `json:"name" jsonschema_description:"Asset name"` - Value float64 `json:"value" jsonschema_description:"Current value"` - Currency string `json:"currency" jsonschema_description:"Currency code"` + ID string `json:"id" jsonschema:"Asset identifier"` + Name string `json:"name" jsonschema:"Asset name"` + Value float64 `json:"value" jsonschema:"Current value"` + Currency string `json:"currency" jsonschema:"Currency code"` } // Tool that returns array of assets assetsTool := mcp.NewTool("list_assets", mcp.WithDescription("List portfolio assets"), mcp.WithInputSchema[struct { - Portfolio string `json:"portfolio" jsonschema_description:"Portfolio ID" jsonschema:"required"` + Portfolio string `json:"portfolio" jsonschema:"Portfolio ID"` }](), mcp.WithOutputSchema[[]Asset](), // Array output schema ) @@ -248,27 +248,21 @@ func listAssetsHandler(ctx context.Context, req mcp.CallToolRequest, args struct ### Schema Tags Reference -MCP-Go uses the `jsonschema` struct tags for schema generation: +MCP-Go uses the `jsonschema` struct tag for schema generation: ```go type ExampleStruct struct { - // Required field - Name string `json:"name" jsonschema:"required"` - + // Required field (no omitempty in json tag means required) + Name string `json:"name" jsonschema:"User's full name"` + // Field with description - Age int `json:"age" jsonschema_description:"User age in years"` - - // Field with constraints - Score float64 `json:"score" jsonschema:"minimum=0,maximum=100"` - - // Enum field - Status string `json:"status" jsonschema:"enum=active,enum=inactive,enum=pending"` - - // Optional field with default - PageSize int `json:"pageSize,omitempty" jsonschema:"default=20"` - - // Array with constraints - Tags []string `json:"tags" jsonschema:"minItems=1,maxItems=10"` + Age int `json:"age" jsonschema:"User age in years"` + + // Optional field (omitempty makes it optional) + PageSize int `json:"pageSize,omitempty" jsonschema:"Number of items per page"` + + // Optional field with description + Status string `json:"status,omitempty" jsonschema:"Current status"` } ``` @@ -308,19 +302,19 @@ Here's a complete example using the file operations pattern from earlier, enhanc ```go // Define structured input for file operations type FileOperationRequest struct { - Path string `json:"path" jsonschema_description:"File path" jsonschema:"required"` - Content string `json:"content,omitempty" jsonschema_description:"File content (for write operations)"` - Encoding string `json:"encoding,omitempty" jsonschema_description:"File encoding" jsonschema:"enum=utf-8,enum=ascii,enum=base64,default=utf-8"` + Path string `json:"path" jsonschema:"File path"` + Content string `json:"content,omitempty" jsonschema:"File content (for write operations)"` + Encoding string `json:"encoding,omitempty" jsonschema:"File encoding"` } // Define structured output type FileOperationResponse struct { - Success bool `json:"success" jsonschema_description:"Operation success status"` - Path string `json:"path" jsonschema_description:"File path"` - Message string `json:"message" jsonschema_description:"Result message"` - Content string `json:"content,omitempty" jsonschema_description:"File content (for read operations)"` - Size int64 `json:"size,omitempty" jsonschema_description:"File size in bytes"` - Modified time.Time `json:"modified,omitempty" jsonschema_description:"Last modified time"` + Success bool `json:"success" jsonschema:"Operation success status"` + Path string `json:"path" jsonschema:"File path"` + Message string `json:"message" jsonschema:"Result message"` + Content string `json:"content,omitempty" jsonschema:"File content (for read operations)"` + Size int64 `json:"size,omitempty" jsonschema:"File size in bytes"` + Modified time.Time `json:"modified,omitempty" jsonschema:"Last modified time"` } func main() { @@ -339,7 +333,7 @@ func main() { readFileTool := mcp.NewTool("read_file", mcp.WithDescription("Read file contents"), mcp.WithInputSchema[struct { - Path string `json:"path" jsonschema_description:"File path to read" jsonschema:"required"` + Path string `json:"path" jsonschema:"File path to read"` }](), mcp.WithOutputSchema[FileOperationResponse](), )