Repository: mark3labs/mcp-go

Issue:
feature: withAny tool argument
## Problem Statement

There's no way to specify that a tool argument can be any shape. withNumber, withBoolean, withArray, withObject, withString exist, but there's nothing for a schema-less type. 

## Proposed Solution

JSON Schema defines the any type as an empty object:
```
"properties": {
        "F1": {
            "type": "string"
        },
        "F2": {
            "type": "integer"
        },
        "F3": {}
    },
```
where F3 is any. 

I'd like to see a withAny tool argument method.


## Example Usage

```go

	tool := mcp.NewTool(
		toolName,
		mcp.WithDescription(toolDescription),
		mcp.WithAny("freeEntry"),
	)
```


Discussion:
@pottekkat: Thank for opening this issue. This seems like a valid request. You can currently use the workaround with `NewToolWithRawSchema()` but this means you have to define the whole schema manually. But the `WithAny` helper function can be added.@1garo: @pottekkat  I would like to contribute, could I start from this one?@pottekkat: Thank you @1garo. Feel free to open a pull request.@1garo: @0xNF could you please give us an example on how you would use it? It would also be useful when adding it to the examples. I couldn't think one.@0xNF: I can just paste some of the actual OpenAPI spec I'm working with:

```yaml
RunRequest:
      required:
      - id
      type: object
      properties:
        id:
          type: string
          description: Id of the item to execute
        in:
          description: Initial input to the execution
          nullable: true
        extra:
          type: object
          additionalProperties: {}
          description: json map of extra metadata
```

As you can see, the `in` can be any format. It's a primitive, a list of primitives, an object of primitives, a list of complex types, an object of complex types, nulls, not nulls, etc. It's a freeform field. Representing this in the current sdk is difficult.

Solution patch:
diff --git a/client/transport/oauth.go b/client/transport/oauth.go
index bb702ad..0fce1d8 100644
--- a/client/transport/oauth.go
+++ b/client/transport/oauth.go
@@ -34,6 +34,9 @@ type OAuthConfig struct {
 	AuthServerMetadataURL string
 	// PKCEEnabled enables PKCE for the OAuth flow (recommended for public clients)
 	PKCEEnabled bool
+	// HTTPClient is an optional HTTP client to use for requests.
+	// If nil, a default HTTP client with a 30 second timeout will be used.
+	HTTPClient *http.Client
 }
 
 // TokenStore is an interface for storing and retrieving OAuth tokens.
@@ -151,10 +154,13 @@ func NewOAuthHandler(config OAuthConfig) *OAuthHandler {
 	if config.TokenStore == nil {
 		config.TokenStore = NewMemoryTokenStore()
 	}
+	if config.HTTPClient == nil {
+		config.HTTPClient = &http.Client{Timeout: 30 * time.Second}
+	}
 
 	return &OAuthHandler{
 		config:     config,
-		httpClient: &http.Client{Timeout: 30 * time.Second},
+		httpClient: config.HTTPClient,
 	}
 }
 
diff --git a/examples/typed_tools/main.go b/examples/typed_tools/main.go
index f9bd3c2..82b1b6c 100644
--- a/examples/typed_tools/main.go
+++ b/examples/typed_tools/main.go
@@ -18,6 +18,7 @@ type GreetingArgs struct {
 		Location string `json:"location"`
 		Timezone string `json:"timezone"`
 	} `json:"metadata"`
+	AnyData any `json:"any_data"`
 }
 
 func main() {
@@ -61,6 +62,9 @@ func main() {
 				},
 			}),
 		),
+		mcp.WithAny("any_data",
+			mcp.Description("Any kind of data, e.g., an integer"),
+		),
 	)
 
 	// Add tool handler using the typed handler
@@ -101,5 +105,9 @@ func typedGreetingHandler(ctx context.Context, request mcp.CallToolRequest, args
 		}
 	}
 
+	if args.AnyData != nil {
+		greeting += fmt.Sprintf(" I also received some other data: %v.", args.AnyData)
+	}
+
 	return mcp.NewToolResultText(greeting), nil
 }
diff --git a/mcp/tools.go b/mcp/tools.go
index fc4a644..9d539f9 100644
--- a/mcp/tools.go
+++ b/mcp/tools.go
@@ -1113,6 +1113,26 @@ func WithArray(name string, opts ...PropertyOption) ToolOption {
 	}
 }
 
+// WithAny adds a property of any type to the tool schema.
+// It accepts property options to configure the property's behavior and constraints.
+func WithAny(name string, opts ...PropertyOption) ToolOption {
+	return func(t *Tool) {
+		schema := map[string]any{}
+
+		for _, opt := range opts {
+			opt(schema)
+		}
+
+		// Remove required from property schema and add to InputSchema.required
+		if required, ok := schema["required"].(bool); ok && required {
+			delete(schema, "required")
+			t.InputSchema.Required = append(t.InputSchema.Required, name)
+		}
+
+		t.InputSchema.Properties[name] = schema
+	}
+}
+
 // Properties defines the properties for an object schema
 func Properties(props map[string]any) PropertyOption {
 	return func(schema map[string]any) {



Test patch:
diff --git a/mcp/tools_test.go b/mcp/tools_test.go
index d172363..16aeb5d 100644
--- a/mcp/tools_test.go
+++ b/mcp/tools_test.go
@@ -242,6 +242,108 @@ func TestToolWithObjectAndArray(t *testing.T) {
 	assert.Contains(t, required, "books")
 }
 
+func TestToolWithAny(t *testing.T) {
+	const desc = "Can be any value: string, number, bool, object, or slice"
+
+	tool := NewTool("any-tool",
+		WithDescription("A tool with an 'any' type property"),
+		WithAny("data",
+			Description(desc),
+			Required(),
+		),
+	)
+
+	data, err := json.Marshal(tool)
+	assert.NoError(t, err)
+
+	var result map[string]any
+	err = json.Unmarshal(data, &result)
+	assert.NoError(t, err)
+
+	assert.Equal(t, "any-tool", result["name"])
+
+	schema, ok := result["inputSchema"].(map[string]any)
+	assert.True(t, ok)
+	assert.Equal(t, "object", schema["type"])
+
+	properties, ok := schema["properties"].(map[string]any)
+	assert.True(t, ok)
+
+	dataProp, ok := properties["data"].(map[string]any)
+	assert.True(t, ok)
+	_, typeExists := dataProp["type"]
+	assert.False(t, typeExists, "The 'any' type property should not have a 'type' field")
+	assert.Equal(t, desc, dataProp["description"])
+
+	required, ok := schema["required"].([]any)
+	assert.True(t, ok)
+	assert.Contains(t, required, "data")
+
+	type testStruct struct {
+		A string `json:"A"`
+	}
+	testCases := []struct {
+		description string
+		arg         any
+		expect      any
+	}{{
+		description: "string",
+		arg:         "hello world",
+		expect:      "hello world",
+	}, {
+		description: "integer",
+		arg:         123,
+		expect:      float64(123), // JSON unmarshals numbers to float64
+	}, {
+		description: "float",
+		arg:         3.14,
+		expect:      3.14,
+	}, {
+		description: "boolean",
+		arg:         true,
+		expect:      true,
+	}, {
+		description: "object",
+		arg:         map[string]any{"key": "value"},
+		expect:      map[string]any{"key": "value"},
+	}, {
+		description: "slice",
+		arg:         []any{1, "two", false},
+		expect:      []any{float64(1), "two", false},
+	}, {
+		description: "struct",
+		arg:         testStruct{A: "B"},
+		expect:      map[string]any{"A": "B"},
+	}}
+
+	for _, tc := range testCases {
+		t.Run(fmt.Sprintf("with_%s", tc.description), func(t *testing.T) {
+			req := CallToolRequest{
+				Request: Request{},
+				Params: CallToolParams{
+					Name: "any-tool",
+					Arguments: map[string]any{
+						"data": tc.arg,
+					},
+				},
+			}
+
+			// Marshal and unmarshal to simulate a real request
+			reqBytes, err := json.Marshal(req)
+			assert.NoError(t, err)
+
+			var unmarshaledReq CallToolRequest
+			err = json.Unmarshal(reqBytes, &unmarshaledReq)
+			assert.NoError(t, err)
+
+			args := unmarshaledReq.GetArguments()
+			value, ok := args["data"]
+			assert.True(t, ok)
+			assert.Equal(t, tc.expect, value)
+		})
+	}
+}
+
 func TestParseToolCallToolRequest(t *testing.T) {
 	request := CallToolRequest{}
 	request.Params.Name = "test-tool"