Skip to content

Commit 881e142

Browse files
authored
feat(mcp): Add WithAny for flexible tool properties (#618)
Introduces `mcp.WithAny`, a new tool option that allows defining a property of `any` type. This is useful for creating tools that can accept a variety of data structures for a given parameter. - `mcp.WithAny` function: A new WithAny function has been added to to support properties of any type in tool schemas. This allows for more flexible tool definitions where a property can accept a string, number, boolean, object, slice, etc. - Testing: `TestToolWithAny`, has been introduced ensure the `WithAny` functionality is working correctly. The tests cover various data types to validate that the schema is correctly generated and that arguments are properly parsed. - Example Usage: The `typed_tools`` example has been updated to demonstrate how to use `mcp.WithAny`. This includes adding a new field of type any to the arguments struct and updating the tool handler to process the new field
1 parent c514979 commit 881e142

File tree

3 files changed

+130
-0
lines changed

3 files changed

+130
-0
lines changed

examples/typed_tools/main.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ type GreetingArgs struct {
1818
Location string `json:"location"`
1919
Timezone string `json:"timezone"`
2020
} `json:"metadata"`
21+
AnyData any `json:"any_data"`
2122
}
2223

2324
func main() {
@@ -61,6 +62,9 @@ func main() {
6162
},
6263
}),
6364
),
65+
mcp.WithAny("any_data",
66+
mcp.Description("Any kind of data, e.g., an integer"),
67+
),
6468
)
6569

6670
// Add tool handler using the typed handler
@@ -101,5 +105,9 @@ func typedGreetingHandler(ctx context.Context, request mcp.CallToolRequest, args
101105
}
102106
}
103107

108+
if args.AnyData != nil {
109+
greeting += fmt.Sprintf(" I also received some other data: %v.", args.AnyData)
110+
}
111+
104112
return mcp.NewToolResultText(greeting), nil
105113
}

mcp/tools.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1113,6 +1113,26 @@ func WithArray(name string, opts ...PropertyOption) ToolOption {
11131113
}
11141114
}
11151115

1116+
// WithAny adds a property of any type to the tool schema.
1117+
// It accepts property options to configure the property's behavior and constraints.
1118+
func WithAny(name string, opts ...PropertyOption) ToolOption {
1119+
return func(t *Tool) {
1120+
schema := map[string]any{}
1121+
1122+
for _, opt := range opts {
1123+
opt(schema)
1124+
}
1125+
1126+
// Remove required from property schema and add to InputSchema.required
1127+
if required, ok := schema["required"].(bool); ok && required {
1128+
delete(schema, "required")
1129+
t.InputSchema.Required = append(t.InputSchema.Required, name)
1130+
}
1131+
1132+
t.InputSchema.Properties[name] = schema
1133+
}
1134+
}
1135+
11161136
// Properties defines the properties for an object schema
11171137
func Properties(props map[string]any) PropertyOption {
11181138
return func(schema map[string]any) {

mcp/tools_test.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,108 @@ func TestToolWithObjectAndArray(t *testing.T) {
242242
assert.Contains(t, required, "books")
243243
}
244244

245+
func TestToolWithAny(t *testing.T) {
246+
const desc = "Can be any value: string, number, bool, object, or slice"
247+
248+
tool := NewTool("any-tool",
249+
WithDescription("A tool with an 'any' type property"),
250+
WithAny("data",
251+
Description(desc),
252+
Required(),
253+
),
254+
)
255+
256+
data, err := json.Marshal(tool)
257+
assert.NoError(t, err)
258+
259+
var result map[string]any
260+
err = json.Unmarshal(data, &result)
261+
assert.NoError(t, err)
262+
263+
assert.Equal(t, "any-tool", result["name"])
264+
265+
schema, ok := result["inputSchema"].(map[string]any)
266+
assert.True(t, ok)
267+
assert.Equal(t, "object", schema["type"])
268+
269+
properties, ok := schema["properties"].(map[string]any)
270+
assert.True(t, ok)
271+
272+
dataProp, ok := properties["data"].(map[string]any)
273+
assert.True(t, ok)
274+
_, typeExists := dataProp["type"]
275+
assert.False(t, typeExists, "The 'any' type property should not have a 'type' field")
276+
assert.Equal(t, desc, dataProp["description"])
277+
278+
required, ok := schema["required"].([]any)
279+
assert.True(t, ok)
280+
assert.Contains(t, required, "data")
281+
282+
type testStruct struct {
283+
A string `json:"A"`
284+
}
285+
testCases := []struct {
286+
description string
287+
arg any
288+
expect any
289+
}{{
290+
description: "string",
291+
arg: "hello world",
292+
expect: "hello world",
293+
}, {
294+
description: "integer",
295+
arg: 123,
296+
expect: float64(123), // JSON unmarshals numbers to float64
297+
}, {
298+
description: "float",
299+
arg: 3.14,
300+
expect: 3.14,
301+
}, {
302+
description: "boolean",
303+
arg: true,
304+
expect: true,
305+
}, {
306+
description: "object",
307+
arg: map[string]any{"key": "value"},
308+
expect: map[string]any{"key": "value"},
309+
}, {
310+
description: "slice",
311+
arg: []any{1, "two", false},
312+
expect: []any{float64(1), "two", false},
313+
}, {
314+
description: "struct",
315+
arg: testStruct{A: "B"},
316+
expect: map[string]any{"A": "B"},
317+
}}
318+
319+
for _, tc := range testCases {
320+
t.Run(fmt.Sprintf("with_%s", tc.description), func(t *testing.T) {
321+
req := CallToolRequest{
322+
Request: Request{},
323+
Params: CallToolParams{
324+
Name: "any-tool",
325+
Arguments: map[string]any{
326+
"data": tc.arg,
327+
},
328+
},
329+
}
330+
331+
// Marshal and unmarshal to simulate a real request
332+
reqBytes, err := json.Marshal(req)
333+
assert.NoError(t, err)
334+
335+
var unmarshaledReq CallToolRequest
336+
err = json.Unmarshal(reqBytes, &unmarshaledReq)
337+
assert.NoError(t, err)
338+
339+
args := unmarshaledReq.GetArguments()
340+
value, ok := args["data"]
341+
assert.True(t, ok)
342+
assert.Equal(t, tc.expect, value)
343+
})
344+
}
345+
}
346+
245347
func TestParseToolCallToolRequest(t *testing.T) {
246348
request := CallToolRequest{}
247349
request.Params.Name = "test-tool"

0 commit comments

Comments
 (0)