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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,15 @@ Defined in the MCP spec here: https://modelcontextprotocol.io/specification/2025

## Usage

Define a struct for your input:

```go
type WeatherRequest struct {
Location string `json:"location,required" jsonschema_description:"City or location"`
Units string `json:"units,omitempty" jsonschema_description:"celsius or fahrenheit" jsonschema:"enum=celsius,enum=fahrenheit"`
}
```

Define a struct for your output:

```go
Expand All @@ -21,8 +30,8 @@ Add it to your tool:
```go
tool := mcp.NewTool("get_weather",
mcp.WithDescription("Get weather information"),
mcp.WithInputSchema[WeatherRequest](),
mcp.WithOutputSchema[WeatherResponse](),
mcp.WithString("location", mcp.Required()),
)
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import (
// Note: The jsonschema_description 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"`
Units string `json:"units,omitempty" jsonschema_description:"celsius or fahrenheit"`
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"`
}

type WeatherResponse struct {
Expand All @@ -32,7 +32,7 @@ type UserProfile struct {
}

type UserRequest struct {
UserID string `json:"userId" jsonschema_description:"User ID"`
UserID string `json:"userId" jsonschema_description:"User ID" jsonschema:"required"`
}

type Asset struct {
Expand All @@ -43,46 +43,45 @@ type Asset struct {
}

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

func main() {
s := server.NewMCPServer(
"Structured Output Example",
"Structured Input/Output Example",
"1.0.0",
server.WithToolCapabilities(false),
)

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

// Example 2: Nested struct schema
userTool := mcp.NewTool("get_user_profile",
mcp.WithDescription("Get user profile"),
mcp.WithInputSchema[UserRequest](),
mcp.WithOutputSchema[UserProfile](),
mcp.WithString("userId", mcp.Required()),
)
s.AddTool(userTool, mcp.NewStructuredToolHandler(getUserProfileHandler))

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

// Example 4: Manual result creation
manualTool := mcp.NewTool("manual_structured",
mcp.WithDescription("Manual structured result"),
mcp.WithInputSchema[WeatherRequest](),
mcp.WithOutputSchema[WeatherResponse](),
mcp.WithString("location", mcp.Required()),
)
s.AddTool(manualTool, mcp.NewTypedToolHandler(manualWeatherHandler))

Expand Down
41 changes: 41 additions & 0 deletions mcp/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,47 @@ func WithDescription(description string) ToolOption {
}
}

// WithInputSchema creates a ToolOption that sets the input schema for a tool.
// 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 := 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
}

t.InputSchema.Type = ""
t.RawInputSchema = json.RawMessage(mcpSchema)
}
}

// WithRawInputSchema sets a raw JSON schema for the tool's input.
// Use this when you need full control over the schema or when working with
// complex schemas that can't be generated from Go types. The jsonschema library
// can handle complex schemas and provides nice extension points, so be sure to
// check that out before using this.
func WithRawInputSchema(schema json.RawMessage) ToolOption {
return func(t *Tool) {
t.RawInputSchema = schema
}
}

// WithOutputSchema creates a ToolOption that sets the output schema for a tool.
// It accepts any Go type, usually a struct, and automatically generates a JSON schema from it.
func WithOutputSchema[T any]() ToolOption {
Expand Down
49 changes: 49 additions & 0 deletions mcp/tools_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,55 @@ func TestFlexibleArgumentsJSONMarshalUnmarshal(t *testing.T) {
assert.Equal(t, float64(123), args["key2"]) // JSON numbers are unmarshaled as float64
}

// TestToolWithInputSchema tests that the WithInputSchema function
// 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"`
}

tool := NewTool("test_tool",
WithDescription("Test tool with output schema"),
WithInputSchema[TestInput](),
)

// Check that RawOutputSchema was set
assert.NotNil(t, tool.RawInputSchema)

// Marshal and verify structure
data, err := json.Marshal(tool)
assert.NoError(t, err)

var toolData map[string]any
err = json.Unmarshal(data, &toolData)
assert.NoError(t, err)

// Verify inputSchema exists
inputSchema, exists := toolData["inputSchema"]
assert.True(t, exists)
assert.NotNil(t, inputSchema)

// Verify required list exists
schemaMap, ok := inputSchema.(map[string]interface{})
assert.True(t, ok)
requiredList, exists := schemaMap["required"]
assert.True(t, exists)
assert.NotNil(t, requiredList)

// Verify properties exist
properties, exists := schemaMap["properties"]
assert.True(t, exists)
propertiesMap, ok := properties.(map[string]interface{})
assert.True(t, ok)

// Verify specific properties
assert.Contains(t, propertiesMap, "name")
assert.Contains(t, propertiesMap, "age")
assert.Contains(t, propertiesMap, "email")
}

// TestToolWithOutputSchema tests that the WithOutputSchema function
// generates an MCP-compatible JSON output schema for a tool
func TestToolWithOutputSchema(t *testing.T) {
Expand Down
Loading