Skip to content

Commit 8a18f59

Browse files
Grivnclaude
andauthored
feat: support creating tools using go-struct-style input schema (#534)
* feat: support creating tools using go-struct-style input schema * docs: add struct-based schema documentation for tools Add comprehensive documentation for the new struct-based schema features introduced in commit b46b0d7, including: - Input schema definition using Go structs with WithInputSchema - Output schema definition using WithOutputSchema - Structured tool handlers with NewStructuredToolHandler - Array output schemas - Schema tags reference - Complete file operations example with structured I/O 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent a3d34d9 commit 8a18f59

File tree

5 files changed

+413
-11
lines changed

5 files changed

+413
-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
@@ -714,6 +714,47 @@ func WithDescription(description string) ToolOption {
714714
}
715715
}
716716

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

mcp/tools_test.go

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

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

0 commit comments

Comments
 (0)