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
36 changes: 18 additions & 18 deletions examples/structured_input_and_output/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
6 changes: 1 addition & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
17 changes: 4 additions & 13 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand All @@ -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=
Expand Down
38 changes: 7 additions & 31 deletions mcp/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
74 changes: 68 additions & 6 deletions mcp/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading