Skip to content

Commit a73d7cf

Browse files
authored
tools: add WithObject and WithArray to supplement type definition utility (#47)
1 parent 6877f7d commit a73d7cf

File tree

2 files changed

+216
-1
lines changed

2 files changed

+216
-1
lines changed

mcp/tools.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,120 @@ func WithString(name string, opts ...PropertyOption) ToolOption {
367367
t.InputSchema.Properties[name] = schema
368368
}
369369
}
370+
371+
// WithObject adds an object property to the tool schema.
372+
// It accepts property options to configure the object property's behavior and constraints.
373+
func WithObject(name string, opts ...PropertyOption) ToolOption {
374+
return func(t *Tool) {
375+
schema := map[string]interface{}{
376+
"type": "object",
377+
"properties": map[string]interface{}{},
378+
}
379+
380+
for _, opt := range opts {
381+
opt(schema)
382+
}
383+
384+
// Remove required from property schema and add to InputSchema.required
385+
if required, ok := schema["required"].(bool); ok && required {
386+
delete(schema, "required")
387+
if t.InputSchema.Required == nil {
388+
t.InputSchema.Required = []string{name}
389+
} else {
390+
t.InputSchema.Required = append(t.InputSchema.Required, name)
391+
}
392+
}
393+
394+
t.InputSchema.Properties[name] = schema
395+
}
396+
}
397+
398+
// WithArray adds an array property to the tool schema.
399+
// It accepts property options to configure the array property's behavior and constraints.
400+
func WithArray(name string, opts ...PropertyOption) ToolOption {
401+
return func(t *Tool) {
402+
schema := map[string]interface{}{
403+
"type": "array",
404+
}
405+
406+
for _, opt := range opts {
407+
opt(schema)
408+
}
409+
410+
// Remove required from property schema and add to InputSchema.required
411+
if required, ok := schema["required"].(bool); ok && required {
412+
delete(schema, "required")
413+
if t.InputSchema.Required == nil {
414+
t.InputSchema.Required = []string{name}
415+
} else {
416+
t.InputSchema.Required = append(t.InputSchema.Required, name)
417+
}
418+
}
419+
420+
t.InputSchema.Properties[name] = schema
421+
}
422+
}
423+
424+
// Properties defines the properties for an object schema
425+
func Properties(props map[string]interface{}) PropertyOption {
426+
return func(schema map[string]interface{}) {
427+
schema["properties"] = props
428+
}
429+
}
430+
431+
// AdditionalProperties specifies whether additional properties are allowed in the object
432+
// or defines a schema for additional properties
433+
func AdditionalProperties(schema interface{}) PropertyOption {
434+
return func(schemaMap map[string]interface{}) {
435+
schemaMap["additionalProperties"] = schema
436+
}
437+
}
438+
439+
// MinProperties sets the minimum number of properties for an object
440+
func MinProperties(min int) PropertyOption {
441+
return func(schema map[string]interface{}) {
442+
schema["minProperties"] = min
443+
}
444+
}
445+
446+
// MaxProperties sets the maximum number of properties for an object
447+
func MaxProperties(max int) PropertyOption {
448+
return func(schema map[string]interface{}) {
449+
schema["maxProperties"] = max
450+
}
451+
}
452+
453+
// PropertyNames defines a schema for property names in an object
454+
func PropertyNames(schema map[string]interface{}) PropertyOption {
455+
return func(schemaMap map[string]interface{}) {
456+
schemaMap["propertyNames"] = schema
457+
}
458+
}
459+
460+
// Items defines the schema for array items
461+
func Items(schema interface{}) PropertyOption {
462+
return func(schemaMap map[string]interface{}) {
463+
schemaMap["items"] = schema
464+
}
465+
}
466+
467+
// MinItems sets the minimum number of items for an array
468+
func MinItems(min int) PropertyOption {
469+
return func(schema map[string]interface{}) {
470+
schema["minItems"] = min
471+
}
472+
}
473+
474+
// MaxItems sets the maximum number of items for an array
475+
func MaxItems(max int) PropertyOption {
476+
return func(schema map[string]interface{}) {
477+
schema["maxItems"] = max
478+
}
479+
}
480+
481+
// UniqueItems specifies whether array items must be unique
482+
func UniqueItems(unique bool) PropertyOption {
483+
return func(schema map[string]interface{}) {
484+
schema["uniqueItems"] = unique
485+
}
486+
}

mcp/tools_test.go

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,102 @@ func TestUnmarshalToolWithoutRawSchema(t *testing.T) {
141141
})
142142
assert.Empty(t, toolUnmarshalled.InputSchema.Required)
143143
assert.Empty(t, toolUnmarshalled.RawInputSchema)
144-
}
144+
}
145+
146+
func TestToolWithObjectAndArray(t *testing.T) {
147+
// Create a tool with both object and array properties
148+
tool := NewTool("reading-list",
149+
WithDescription("A tool for managing reading lists"),
150+
WithObject("preferences",
151+
Description("User preferences for the reading list"),
152+
Properties(map[string]interface{}{
153+
"theme": map[string]interface{}{
154+
"type": "string",
155+
"description": "UI theme preference",
156+
"enum": []string{"light", "dark"},
157+
},
158+
"maxItems": map[string]interface{}{
159+
"type": "number",
160+
"description": "Maximum number of items in the list",
161+
"minimum": 1,
162+
"maximum": 100,
163+
},
164+
})),
165+
WithArray("books",
166+
Description("List of books to read"),
167+
Required(),
168+
Items(map[string]interface{}{
169+
"type": "object",
170+
"properties": map[string]interface{}{
171+
"title": map[string]interface{}{
172+
"type": "string",
173+
"description": "Book title",
174+
"required": true,
175+
},
176+
"author": map[string]interface{}{
177+
"type": "string",
178+
"description": "Book author",
179+
},
180+
"year": map[string]interface{}{
181+
"type": "number",
182+
"description": "Publication year",
183+
"minimum": 1000,
184+
},
185+
},
186+
})))
187+
188+
// Marshal to JSON
189+
data, err := json.Marshal(tool)
190+
assert.NoError(t, err)
191+
192+
// Unmarshal to verify the structure
193+
var result map[string]interface{}
194+
err = json.Unmarshal(data, &result)
195+
assert.NoError(t, err)
196+
197+
// Verify tool properties
198+
assert.Equal(t, "reading-list", result["name"])
199+
assert.Equal(t, "A tool for managing reading lists", result["description"])
200+
201+
// Verify schema was properly included
202+
schema, ok := result["inputSchema"].(map[string]interface{})
203+
assert.True(t, ok)
204+
assert.Equal(t, "object", schema["type"])
205+
206+
// Verify properties
207+
properties, ok := schema["properties"].(map[string]interface{})
208+
assert.True(t, ok)
209+
210+
// Verify preferences object
211+
preferences, ok := properties["preferences"].(map[string]interface{})
212+
assert.True(t, ok)
213+
assert.Equal(t, "object", preferences["type"])
214+
assert.Equal(t, "User preferences for the reading list", preferences["description"])
215+
216+
prefProps, ok := preferences["properties"].(map[string]interface{})
217+
assert.True(t, ok)
218+
assert.Contains(t, prefProps, "theme")
219+
assert.Contains(t, prefProps, "maxItems")
220+
221+
// Verify books array
222+
books, ok := properties["books"].(map[string]interface{})
223+
assert.True(t, ok)
224+
assert.Equal(t, "array", books["type"])
225+
assert.Equal(t, "List of books to read", books["description"])
226+
227+
// Verify array items schema
228+
items, ok := books["items"].(map[string]interface{})
229+
assert.True(t, ok)
230+
assert.Equal(t, "object", items["type"])
231+
232+
itemProps, ok := items["properties"].(map[string]interface{})
233+
assert.True(t, ok)
234+
assert.Contains(t, itemProps, "title")
235+
assert.Contains(t, itemProps, "author")
236+
assert.Contains(t, itemProps, "year")
237+
238+
// Verify required fields
239+
required, ok := schema["required"].([]interface{})
240+
assert.True(t, ok)
241+
assert.Contains(t, required, "books")
242+
}

0 commit comments

Comments
 (0)