88 "io"
99 nethttp "net/http"
1010 neturl "net/url"
11+ "strconv"
1112 "strings"
1213
1314 "github.com/google/jsonschema-go/jsonschema"
@@ -29,6 +30,8 @@ type HttpInvoker struct {
2930 Method string // Http request method
3031 InputSchema * jsonschema.Resolved // InputSchema for the tool
3132 URITemplate string // MCP URI template (for resource templates only)
33+ BodyRoot string // Dot-separated path to extract as the request body
34+ BodyAsArray bool // Wrap the entire body in a JSON array
3235}
3336
3437var _ invocation.Invoker = & HttpInvoker {}
@@ -372,6 +375,8 @@ func (hi *HttpInvoker) executeHTTPRequest(
372375
373376// prepareRequestBody creates a JSON body from the parsed arguments,
374377// excluding any variables that are used in the URL template or header templates
378+ // if BodyRoot is set, it extracts that property's value as the body
379+ // if BodyAsArray is set, it wraps the entire body in a JSON array
375380func (hi * HttpInvoker ) prepareRequestBody (parsed map [string ]any ) ([]byte , error ) {
376381 varNames := make ([]string , 0 , len (hi .ParsedTemplate .Variables ))
377382 for _ , v := range hi .ParsedTemplate .Variables {
@@ -384,7 +389,20 @@ func (hi *HttpInvoker) prepareRequestBody(parsed map[string]any) ([]byte, error)
384389 }
385390 }
386391
387- return json .Marshal (deletePathsFromMap (parsed , varNames ))
392+ var body any = deletePathsFromMap (parsed , varNames )
393+
394+ if hi .BodyRoot != "" {
395+ val , ok := getValueByPath (parsed , hi .BodyRoot )
396+ if ! ok {
397+ return nil , fmt .Errorf ("bodyRoot property %q not found in arguments" , hi .BodyRoot )
398+ }
399+ body = val
400+ } else if hi .BodyAsArray {
401+ // wrap the body in an array
402+ body = []any {body }
403+ }
404+
405+ return json .Marshal (body )
388406}
389407
390408// buildRequestComponents builds the URL and headers from request arguments and incoming headers.
@@ -653,3 +671,81 @@ func deletePathFromMap(m map[string]any, path string) {
653671 delete (parentMap , keys [len (keys )- 2 ])
654672 }
655673}
674+
675+ type pathSegment struct {
676+ key string
677+ index int
678+ isIndex bool
679+ }
680+
681+ func getValueByPath (m map [string ]any , path string ) (any , bool ) {
682+ segments , err := parsePathSegments (path )
683+ if err != nil {
684+ return nil , false
685+ }
686+
687+ var current any = m
688+
689+ for _ , seg := range segments {
690+ switch v := current .(type ) {
691+ case map [string ]any :
692+ if seg .isIndex {
693+ return nil , false
694+ }
695+ var ok bool
696+ current , ok = v [seg .key ]
697+ if ! ok {
698+ return nil , false
699+ }
700+ case []any :
701+ if ! seg .isIndex {
702+ return nil , false
703+ }
704+ if seg .index < 0 || seg .index >= len (v ) {
705+ return nil , false
706+ }
707+ current = v [seg .index ]
708+ default :
709+ return nil , false
710+ }
711+ }
712+
713+ return current , true
714+ }
715+
716+ func parsePathSegments (path string ) ([]pathSegment , error ) {
717+ var segments []pathSegment
718+ var err error
719+
720+ for part := range strings .SplitSeq (path , "." ) {
721+ for len (part ) > 0 {
722+ bracketStart := strings .Index (part , "[" )
723+ if bracketStart == - 1 {
724+ if part != "" {
725+ segments = append (segments , pathSegment {key : part })
726+ }
727+ break
728+ }
729+
730+ if bracketStart > 0 {
731+ segments = append (segments , pathSegment {key : part [:bracketStart ]})
732+ }
733+
734+ bracketEnd := strings .Index (part , "]" )
735+ if bracketEnd == - 1 || bracketEnd <= bracketStart + 1 {
736+ return nil , fmt .Errorf ("failed to find matching closing bracket for opening bracket" )
737+ }
738+
739+ indexStr := part [bracketStart + 1 : bracketEnd ]
740+ var index int64
741+ if index , err = strconv .ParseInt (indexStr , 10 , 0 ); err != nil {
742+ return nil , fmt .Errorf ("failed to parse array index as int: %w" , err )
743+ }
744+
745+ segments = append (segments , pathSegment {index : int (index ), isIndex : true })
746+ part = part [bracketEnd + 1 :]
747+ }
748+ }
749+
750+ return segments , nil
751+ }
0 commit comments