Skip to content

Commit b46b0d7

Browse files
committed
feat: support creating tools using go-struct-style input schema
1 parent 9393526 commit b46b0d7

File tree

4 files changed

+109
-11
lines changed

4 files changed

+109
-11
lines changed

examples/structured_output/README.md renamed to examples/structured_input_and_output/README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,15 @@ Defined in the MCP spec here: https://modelcontextprotocol.io/specification/2025
66

77
## Usage
88

9+
Define a struct for your input:
10+
11+
```go
12+
type WeatherRequest struct {
13+
Location string `json:"location,required" jsonschema_description:"City or location"`
14+
Units string `json:"units,omitempty" jsonschema_description:"celsius or fahrenheit" jsonschema:"enum=celsius,enum=fahrenheit"`
15+
}
16+
```
17+
918
Define a struct for your output:
1019

1120
```go
@@ -21,8 +30,8 @@ Add it to your tool:
2130
```go
2231
tool := mcp.NewTool("get_weather",
2332
mcp.WithDescription("Get weather information"),
33+
mcp.WithInputSchema[WeatherRequest](),
2434
mcp.WithOutputSchema[WeatherResponse](),
25-
mcp.WithString("location", mcp.Required()),
2635
)
2736
```
2837

examples/structured_output/main.go renamed to examples/structured_input_and_output/main.go

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import (
1212
// Note: The jsonschema_description tag is added to the JSON schema as description
1313
// Ideally use better descriptions, this is just an example
1414
type WeatherRequest struct {
15-
Location string `json:"location" jsonschema_description:"City or location"`
16-
Units string `json:"units,omitempty" jsonschema_description:"celsius or fahrenheit"`
15+
Location string `json:"location" jsonschema_description:"City or location" jsonschema:"required"`
16+
Units string `json:"units,omitempty" jsonschema_description:"celsius or fahrenheit" jsonschema:"enum=celsius,enum=fahrenheit"`
1717
}
1818

1919
type WeatherResponse struct {
@@ -32,7 +32,7 @@ type UserProfile struct {
3232
}
3333

3434
type UserRequest struct {
35-
UserID string `json:"userId" jsonschema_description:"User ID"`
35+
UserID string `json:"userId" jsonschema_description:"User ID" jsonschema:"required"`
3636
}
3737

3838
type Asset struct {
@@ -43,46 +43,45 @@ type Asset struct {
4343
}
4444

4545
type AssetListRequest struct {
46-
Limit int `json:"limit,omitempty" jsonschema_description:"Number of assets to return"`
46+
Limit int `json:"limit,omitempty" jsonschema_description:"Number of assets to return" jsonschema:"minimum=1,maximum=100,default=10"`
4747
}
4848

4949
func main() {
5050
s := server.NewMCPServer(
51-
"Structured Output Example",
51+
"Structured Input/Output Example",
5252
"1.0.0",
5353
server.WithToolCapabilities(false),
5454
)
5555

5656
// Example 1: Auto-generated schema from struct
5757
weatherTool := mcp.NewTool("get_weather",
5858
mcp.WithDescription("Get weather with structured output"),
59+
mcp.WithInputSchema[WeatherRequest](),
5960
mcp.WithOutputSchema[WeatherResponse](),
60-
mcp.WithString("location", mcp.Required()),
61-
mcp.WithString("units", mcp.Enum("celsius", "fahrenheit"), mcp.DefaultString("celsius")),
6261
)
6362
s.AddTool(weatherTool, mcp.NewStructuredToolHandler(getWeatherHandler))
6463

6564
// Example 2: Nested struct schema
6665
userTool := mcp.NewTool("get_user_profile",
6766
mcp.WithDescription("Get user profile"),
67+
mcp.WithInputSchema[UserRequest](),
6868
mcp.WithOutputSchema[UserProfile](),
69-
mcp.WithString("userId", mcp.Required()),
7069
)
7170
s.AddTool(userTool, mcp.NewStructuredToolHandler(getUserProfileHandler))
7271

7372
// Example 3: Array output - direct array of objects
7473
assetsTool := mcp.NewTool("get_assets",
7574
mcp.WithDescription("Get list of assets as array"),
75+
mcp.WithInputSchema[AssetListRequest](),
7676
mcp.WithOutputSchema[[]Asset](),
77-
mcp.WithNumber("limit", mcp.Min(1), mcp.Max(100), mcp.DefaultNumber(10)),
7877
)
7978
s.AddTool(assetsTool, mcp.NewStructuredToolHandler(getAssetsHandler))
8079

8180
// Example 4: Manual result creation
8281
manualTool := mcp.NewTool("manual_structured",
8382
mcp.WithDescription("Manual structured result"),
83+
mcp.WithInputSchema[WeatherRequest](),
8484
mcp.WithOutputSchema[WeatherResponse](),
85-
mcp.WithString("location", mcp.Required()),
8685
)
8786
s.AddTool(manualTool, mcp.NewTypedToolHandler(manualWeatherHandler))
8887

mcp/tools.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,47 @@ func WithDescription(description string) ToolOption {
704704
}
705705
}
706706

707+
// WithInputSchema creates a ToolOption that sets the input schema for a tool.
708+
// It accepts any Go type, usually a struct, and automatically generates a JSON schema from it.
709+
func WithInputSchema[T any]() ToolOption {
710+
return func(t *Tool) {
711+
var zero T
712+
713+
// Generate schema using invopop/jsonschema library
714+
// Configure reflector to generate clean, MCP-compatible schemas
715+
reflector := jsonschema.Reflector{
716+
DoNotReference: true, // Removes $defs map, outputs entire structure inline
717+
Anonymous: true, // Hides auto-generated Schema IDs
718+
AllowAdditionalProperties: true, // Removes additionalProperties: false
719+
}
720+
schema := reflector.Reflect(zero)
721+
722+
// Clean up schema for MCP compliance
723+
schema.Version = "" // Remove $schema field
724+
725+
// Convert to raw JSON for MCP
726+
mcpSchema, err := json.Marshal(schema)
727+
if err != nil {
728+
// Skip and maintain backward compatibility
729+
return
730+
}
731+
732+
t.InputSchema.Type = ""
733+
t.RawInputSchema = json.RawMessage(mcpSchema)
734+
}
735+
}
736+
737+
// WithRawInputSchema sets a raw JSON schema for the tool's input.
738+
// Use this when you need full control over the schema or when working with
739+
// complex schemas that can't be generated from Go types. The jsonschema library
740+
// can handle complex schemas and provides nice extension points, so be sure to
741+
// check that out before using this.
742+
func WithRawInputSchema(schema json.RawMessage) ToolOption {
743+
return func(t *Tool) {
744+
t.RawInputSchema = schema
745+
}
746+
}
747+
707748
// WithOutputSchema creates a ToolOption that sets the output schema for a tool.
708749
// It accepts any Go type, usually a struct, and automatically generates a JSON schema from it.
709750
func WithOutputSchema[T any]() ToolOption {

mcp/tools_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,55 @@ func TestFlexibleArgumentsJSONMarshalUnmarshal(t *testing.T) {
529529
assert.Equal(t, float64(123), args["key2"]) // JSON numbers are unmarshaled as float64
530530
}
531531

532+
// TestToolWithInputSchema tests that the WithInputSchema function
533+
// generates an MCP-compatible JSON output schema for a tool
534+
func TestToolWithInputSchema(t *testing.T) {
535+
type TestInput struct {
536+
Name string `json:"name" jsonschema_description:"Person's name" jsonschema:"required"`
537+
Age int `json:"age" jsonschema_description:"Person's age"`
538+
Email string `json:"email,omitempty" jsonschema_description:"Email address" jsonschema:"required"`
539+
}
540+
541+
tool := NewTool("test_tool",
542+
WithDescription("Test tool with output schema"),
543+
WithInputSchema[TestInput](),
544+
)
545+
546+
// Check that RawOutputSchema was set
547+
assert.NotNil(t, tool.RawInputSchema)
548+
549+
// Marshal and verify structure
550+
data, err := json.Marshal(tool)
551+
assert.NoError(t, err)
552+
553+
var toolData map[string]any
554+
err = json.Unmarshal(data, &toolData)
555+
assert.NoError(t, err)
556+
557+
// Verify inputSchema exists
558+
inputSchema, exists := toolData["inputSchema"]
559+
assert.True(t, exists)
560+
assert.NotNil(t, inputSchema)
561+
562+
// Verify required list exists
563+
schemaMap, ok := inputSchema.(map[string]interface{})
564+
assert.True(t, ok)
565+
requiredList, exists := schemaMap["required"]
566+
assert.True(t, exists)
567+
assert.NotNil(t, requiredList)
568+
569+
// Verify properties exist
570+
properties, exists := schemaMap["properties"]
571+
assert.True(t, exists)
572+
propertiesMap, ok := properties.(map[string]interface{})
573+
assert.True(t, ok)
574+
575+
// Verify specific properties
576+
assert.Contains(t, propertiesMap, "name")
577+
assert.Contains(t, propertiesMap, "age")
578+
assert.Contains(t, propertiesMap, "email")
579+
}
580+
532581
// TestToolWithOutputSchema tests that the WithOutputSchema function
533582
// generates an MCP-compatible JSON output schema for a tool
534583
func TestToolWithOutputSchema(t *testing.T) {

0 commit comments

Comments
 (0)