Skip to content

Commit 712822b

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

File tree

3 files changed

+60
-11
lines changed

3 files changed

+60
-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,required" jsonschema_description:"City or location"`
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,required" jsonschema_description:"User ID"`
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 {

0 commit comments

Comments
 (0)