Repository: mark3labs/mcp-go

Issue:
feature: API Design Enhancement: Improve type safety for mcpgo.Items() to prevent ToolOption misuse
## Problem Statement

I'm always frustrated when using mcpgo.Items() because it's easy to accidentally pass ToolOption functions instead of schema objects, which causes confusing runtime JSON marshaling errors instead of clear compile-time errors.

While developing our MCP server, we repeatedly made this mistake:

```go
// This looks correct but causes runtime errors
mcpgo.Items(mcpgo.WithString("", mcpgo.Enum("A", "B", "C", "D")))
```

The error only surfaces during JSON serialization with an unhelpful message:

json: error calling MarshalJSON for type mcp.ToolInputSchema: json: unsupported type: mcp.ToolOption

This confusion stems from the similar syntax between ToolOption functions and the expected schema objects, combined with Items() accepting any type which provides no compile-time protection.

## Proposed Solution

I propose adding type-safe helper functions for common array item scenarios while maintaining backward compatibility:

```go
func ItemsString(opts ...PropertyOption) PropertyOption
func ItemsStringWithEnum(values []string) PropertyOption  
func ItemsNumber(opts ...PropertyOption) PropertyOption
func ItemsBoolean(opts ...PropertyOption) PropertyOption
```

Additionally, enhance the existing Items() function with better runtime validation and error messages that guide developers toward the correct usage.

For improved documentation, clearly distinguish between the three different types of configuration objects:

- ToolOption functions for top-level tool configuration
- PropertyOption functions for individual property behavior
- Schema objects for complex nested structures

## MCP Spec Reference

This enhancement relates to the MCP specification's tool definition requirements where tools must provide valid JSON Schema for their input parameters. Better type safety would help ensure compliance with the JSON Schema specification without breaking the MCP protocol.

## Example Usage

```go
// Current verbose but correct approach
mcpgo.WithArray("workload_type",
    mcpgo.Description("Filter by workload types"),
    mcpgo.Items(map[string]any{
        "type": "string",
        "enum": []string{"A", "B", "C", "D"},
    }),
)

// Proposed simplified approach
mcpgo.WithArray("workload_type", 
    mcpgo.Description("Filter by workload types"),
    mcpgo.ItemsStringWithEnum([]string{"A", "B", "C", "D"}),
)

// For simple string arrays
mcpgo.WithArray("os_names",
    mcpgo.Description("Operating system names"),
    mcpgo.ItemsString(),
)
```

## Alternatives/Workarounds Considered

**Current Workaround**: Manually create `map[string]any` objects for all array items, which works but is verbose and error-prone.

**Alternative 1**: Use interfaces or generics to make `Items()` type-safe, but this would break backward compatibility.

**Alternative 2**: Add runtime type checking to `Items()` that panics early with helpful error messages, but this doesn't solve the fundamental usability issue.

**Alternative 3**: Comprehensive documentation improvements alone, but this doesn't address the underlying API design that encourages mistakes.

The proposed helper functions provide the best balance of improved developer experience while maintaining full backward compatibility. Developers can continue using the existing `Items()` function, but have better alternatives available for common use cases.

## Proposed Documentation Enhancement

To further prevent misuse and improve clarity, each proposed helper function (e.g., ItemsString, ItemsStringWithEnum, ItemsNumber, ItemsBoolean) should include detailed GoDoc comments that explicitly show the resulting JSON Schema structure. These comments would help developers understand the expected output and avoid incorrect usage. For example:

```go
// ItemsString configures an array's items to be of type string.
// It accepts optional PropertyOption functions to further customize the schema.
// The resulting JSON Schema will look like:
// {
//   "type": "string"
// }
// Example:
//   mcpgo.WithArray("os_names", mcpgo.ItemsString(mcpgo.Description("OS name")))
func ItemsString(opts ...PropertyOption) PropertyOption

// ItemsStringWithEnum configures an array's items to be of type string with a specified enum.
// The resulting JSON Schema will look like:
// {
//   "type": "string",
//   "enum": ["value1", "value2", ...]
// }
// Example:
//   mcpgo.WithArray("workload_type", mcpgo.ItemsStringWithEnum([]string{"VM", "PC", "PS", "FS"}))
func ItemsStringWithEnum(values []string) PropertyOption
```

Similarly, the existing Items() function should include a comment clarifying its expected input and output schema structure, such as:

```go
// Items specifies the schema for an array's items, accepting either a schema object (e.g., map[string]any) or PropertyOption functions.
// The resulting JSON Schema depends on the provided input, e.g.:
// {
//   "type": "string",
//   "enum": ["VM", "PC"]
// }
// Example:
//   mcpgo.Items(map[string]any{"type": "string", "enum": []string{"VM", "PC"}})
func Items(item any) PropertyOption
```

These comments would make it immediately clear what each function produces, reducing the likelihood of passing incorrect types and improving the developer experience by aligning expectations with the MCP's JSON Schema requirements.

I'd be happy to discuss the implementation approach and contribute a pull request if this enhancement aligns with the project's goals.

Discussion:
@pottekkat: tagging @ezynda3 @dugenkui03 and @rwjblue-glean as well to check this suggestion.@ezynda3: @davidleitw I would welcome a PR that includes helper functions + godocs. The runtime checking could be nice but doesn't stop people from initially making the same mistake we're trying to avoid.@davidleitw: @ezynda3 Thanks! I’ll start drafting a PR with the helper functions and documentation. I agree — runtime checks aren't a full solution, but hopefully they can reduce some of the friction. Appreciate the feedback!



Solution patch:
diff --git a/mcp/tools.go b/mcp/tools.go
index 5f3524b..3e3931b 100644
--- a/mcp/tools.go
+++ b/mcp/tools.go
@@ -945,7 +945,20 @@ func PropertyNames(schema map[string]any) PropertyOption {
 	}
 }
 
-// Items defines the schema for array items
+// Items defines the schema for array items.
+// Accepts any schema definition for maximum flexibility.
+//
+// Example:
+//
+//	Items(map[string]any{
+//	    "type": "object",
+//	    "properties": map[string]any{
+//	        "name": map[string]any{"type": "string"},
+//	        "age": map[string]any{"type": "number"},
+//	    },
+//	})
+//
+// For simple types, use ItemsString(), ItemsNumber(), ItemsBoolean() instead.
 func Items(schema any) PropertyOption {
 	return func(schemaMap map[string]any) {
 		schemaMap["items"] = schema
@@ -972,3 +985,94 @@ func UniqueItems(unique bool) PropertyOption {
 		schema["uniqueItems"] = unique
 	}
 }
+
+// WithStringItems configures an array's items to be of type string.
+//
+// Supported options: Description(), DefaultString(), Enum(), MaxLength(), MinLength(), Pattern()
+// Note: Options like Required() are not valid for item schemas and will be ignored.
+//
+// Examples:
+//
+//	mcp.WithArray("tags", mcp.WithStringItems())
+//	mcp.WithArray("colors", mcp.WithStringItems(mcp.Enum("red", "green", "blue")))
+//	mcp.WithArray("names", mcp.WithStringItems(mcp.MinLength(1), mcp.MaxLength(50)))
+//
+// Limitations: Only supports simple string arrays. Use Items() for complex objects.
+func WithStringItems(opts ...PropertyOption) PropertyOption {
+	return func(schema map[string]any) {
+		itemSchema := map[string]any{
+			"type": "string",
+		}
+
+		for _, opt := range opts {
+			opt(itemSchema)
+		}
+
+		schema["items"] = itemSchema
+	}
+}
+
+// WithStringEnumItems configures an array's items to be of type string with a specified enum.
+// Example:
+//
+//	mcp.WithArray("priority", mcp.WithStringEnumItems([]string{"low", "medium", "high"}))
+//
+// Limitations: Only supports string enums. Use WithStringItems(Enum(...)) for more flexibility.
+func WithStringEnumItems(values []string) PropertyOption {
+	return func(schema map[string]any) {
+		schema["items"] = map[string]any{
+			"type": "string",
+			"enum": values,
+		}
+	}
+}
+
+// WithNumberItems configures an array's items to be of type number.
+//
+// Supported options: Description(), DefaultNumber(), Min(), Max(), MultipleOf()
+// Note: Options like Required() are not valid for item schemas and will be ignored.
+//
+// Examples:
+//
+//	mcp.WithArray("scores", mcp.WithNumberItems(mcp.Min(0), mcp.Max(100)))
+//	mcp.WithArray("prices", mcp.WithNumberItems(mcp.Min(0)))
+//
+// Limitations: Only supports simple number arrays. Use Items() for complex objects.
+func WithNumberItems(opts ...PropertyOption) PropertyOption {
+	return func(schema map[string]any) {
+		itemSchema := map[string]any{
+			"type": "number",
+		}
+
+		for _, opt := range opts {
+			opt(itemSchema)
+		}
+
+		schema["items"] = itemSchema
+	}
+}
+
+// WithBooleanItems configures an array's items to be of type boolean.
+//
+// Supported options: Description(), DefaultBool()
+// Note: Options like Required() are not valid for item schemas and will be ignored.
+//
+// Examples:
+//
+//	mcp.WithArray("flags", mcp.WithBooleanItems())
+//	mcp.WithArray("permissions", mcp.WithBooleanItems(mcp.Description("User permissions")))
+//
+// Limitations: Only supports simple boolean arrays. Use Items() for complex objects.
+func WithBooleanItems(opts ...PropertyOption) PropertyOption {
+	return func(schema map[string]any) {
+		itemSchema := map[string]any{
+			"type": "boolean",
+		}
+
+		for _, opt := range opts {
+			opt(itemSchema)
+		}
+
+		schema["items"] = itemSchema
+	}
+}



Test patch:
diff --git a/mcp/tools_test.go b/mcp/tools_test.go
index 7f2640b..0cd7123 100644
--- a/mcp/tools_test.go
+++ b/mcp/tools_test.go
@@ -528,3 +528,187 @@ func TestFlexibleArgumentsJSONMarshalUnmarshal(t *testing.T) {
 	assert.Equal(t, "value1", args["key1"])
 	assert.Equal(t, float64(123), args["key2"]) // JSON numbers are unmarshaled as float64
 }
+
+// TestNewItemsAPICompatibility tests that the new Items API functions
+// generate the same schema as the original Items() function with manual schema objects
+func TestNewItemsAPICompatibility(t *testing.T) {
+	tests := []struct {
+		name    string
+		oldTool Tool
+		newTool Tool
+	}{
+		{
+			name: "WithStringItems basic",
+			oldTool: NewTool("old-string-array",
+				WithDescription("Tool with string array using old API"),
+				WithArray("items",
+					Description("List of string items"),
+					Items(map[string]any{
+						"type": "string",
+					}),
+				),
+			),
+			newTool: NewTool("new-string-array",
+				WithDescription("Tool with string array using new API"),
+				WithArray("items",
+					Description("List of string items"),
+					WithStringItems(),
+				),
+			),
+		},
+		{
+			name: "WithStringEnumItems",
+			oldTool: NewTool("old-enum-array",
+				WithDescription("Tool with enum array using old API"),
+				WithArray("status",
+					Description("Filter by status"),
+					Items(map[string]any{
+						"type": "string",
+						"enum": []string{"active", "inactive", "pending"},
+					}),
+				),
+			),
+			newTool: NewTool("new-enum-array",
+				WithDescription("Tool with enum array using new API"),
+				WithArray("status",
+					Description("Filter by status"),
+					WithStringEnumItems([]string{"active", "inactive", "pending"}),
+				),
+			),
+		},
+		{
+			name: "WithStringItems with options",
+			oldTool: NewTool("old-string-with-opts",
+				WithDescription("Tool with string array with options using old API"),
+				WithArray("names",
+					Description("List of names"),
+					Items(map[string]any{
+						"type":      "string",
+						"minLength": 1,
+						"maxLength": 50,
+					}),
+				),
+			),
+			newTool: NewTool("new-string-with-opts",
+				WithDescription("Tool with string array with options using new API"),
+				WithArray("names",
+					Description("List of names"),
+					WithStringItems(MinLength(1), MaxLength(50)),
+				),
+			),
+		},
+		{
+			name: "WithNumberItems basic",
+			oldTool: NewTool("old-number-array",
+				WithDescription("Tool with number array using old API"),
+				WithArray("scores",
+					Description("List of scores"),
+					Items(map[string]any{
+						"type": "number",
+					}),
+				),
+			),
+			newTool: NewTool("new-number-array",
+				WithDescription("Tool with number array using new API"),
+				WithArray("scores",
+					Description("List of scores"),
+					WithNumberItems(),
+				),
+			),
+		},
+		{
+			name: "WithNumberItems with constraints",
+			oldTool: NewTool("old-number-with-constraints",
+				WithDescription("Tool with constrained number array using old API"),
+				WithArray("ratings",
+					Description("List of ratings"),
+					Items(map[string]any{
+						"type":    "number",
+						"minimum": 0.0,
+						"maximum": 10.0,
+					}),
+				),
+			),
+			newTool: NewTool("new-number-with-constraints",
+				WithDescription("Tool with constrained number array using new API"),
+				WithArray("ratings",
+					Description("List of ratings"),
+					WithNumberItems(Min(0), Max(10)),
+				),
+			),
+		},
+		{
+			name: "WithBooleanItems basic",
+			oldTool: NewTool("old-boolean-array",
+				WithDescription("Tool with boolean array using old API"),
+				WithArray("flags",
+					Description("List of feature flags"),
+					Items(map[string]any{
+						"type": "boolean",
+					}),
+				),
+			),
+			newTool: NewTool("new-boolean-array",
+				WithDescription("Tool with boolean array using new API"),
+				WithArray("flags",
+					Description("List of feature flags"),
+					WithBooleanItems(),
+				),
+			),
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			// Marshal both tools to JSON
+			oldData, err := json.Marshal(tt.oldTool)
+			assert.NoError(t, err)
+
+			newData, err := json.Marshal(tt.newTool)
+			assert.NoError(t, err)
+
+			// Unmarshal to maps for comparison
+			var oldResult, newResult map[string]any
+			err = json.Unmarshal(oldData, &oldResult)
+			assert.NoError(t, err)
+
+			err = json.Unmarshal(newData, &newResult)
+			assert.NoError(t, err)
+
+			// Compare the inputSchema properties (ignoring tool names and descriptions)
+			oldSchema := oldResult["inputSchema"].(map[string]any)
+			newSchema := newResult["inputSchema"].(map[string]any)
+
+			oldProperties := oldSchema["properties"].(map[string]any)
+			newProperties := newSchema["properties"].(map[string]any)
+
+			// Get the array property (should be the only one in these tests)
+			var oldArrayProp, newArrayProp map[string]any
+			for _, prop := range oldProperties {
+				if propMap, ok := prop.(map[string]any); ok && propMap["type"] == "array" {
+					oldArrayProp = propMap
+					break
+				}
+			}
+			for _, prop := range newProperties {
+				if propMap, ok := prop.(map[string]any); ok && propMap["type"] == "array" {
+					newArrayProp = propMap
+					break
+				}
+			}
+
+			assert.NotNil(t, oldArrayProp, "Old tool should have array property")
+			assert.NotNil(t, newArrayProp, "New tool should have array property")
+
+			// Compare the items schema - this is the critical part
+			oldItems := oldArrayProp["items"]
+			newItems := newArrayProp["items"]
+
+			assert.Equal(t, oldItems, newItems, "Items schema should be identical between old and new API")
+
+			// Also compare other array properties like description
+			assert.Equal(t, oldArrayProp["description"], newArrayProp["description"], "Array descriptions should match")
+			assert.Equal(t, oldArrayProp["type"], newArrayProp["type"], "Array types should match")
+		})
+	}
+}