Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions internal/tools/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import (
"bytes"
"context"
"fmt"
"maps"
"net/http"
"net/url"
"slices"
Expand Down Expand Up @@ -249,16 +248,26 @@ func getURL(baseURL, path string, pathParams, queryParams parameters.Parameters,
return parsedURL.String(), nil
}

// Merge headers case-insensitively, with later sets overriding earlier sets.
func mergeHeaders(headerSets ...map[string]string) map[string]string {
headers := make(map[string]string)
for _, headerSet := range headerSets {
for name, value := range headerSet {
headers[http.CanonicalHeaderKey(name)] = value
}
}
return headers
}

// Helper function to generate the HTTP headers upon Tool invocation.
func getHeaders(headerParams parameters.Parameters, defaultHeaders map[string]string, paramsMap map[string]any) (map[string]string, error) {
// Populate header params
allHeaders := make(map[string]string)
maps.Copy(allHeaders, defaultHeaders)
allHeaders := mergeHeaders(defaultHeaders)
for _, p := range headerParams {
headerValue, ok := paramsMap[p.GetName()]
if ok {
if strValue, ok := headerValue.(string); ok {
allHeaders[p.GetName()] = strValue
allHeaders[http.CanonicalHeaderKey(p.GetName())] = strValue
} else {
return nil, fmt.Errorf("header param %s got value of type %t, not string", p.GetName(), headerValue)
}
Expand All @@ -274,9 +283,7 @@ func (t Tool) Invoke(ctx context.Context, s sources.Source, params parameters.Pa
}
// Combine Source and Tool headers.
// In case of conflict, Tool header overrides Source header
combinedHeaders := make(map[string]string)
maps.Copy(combinedHeaders, source.HttpDefaultHeaders())
maps.Copy(combinedHeaders, t.Cfg.Headers)
combinedHeaders := mergeHeaders(source.HttpDefaultHeaders(), t.Cfg.Headers)

paramsMap := params.AsMap()

Expand Down
36 changes: 36 additions & 0 deletions internal/tools/http/http_url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,48 @@
package http

import (
"maps"
"net/url"
"testing"

"github.com/googleapis/mcp-toolbox/internal/util/parameters"
)

func TestGetHeadersCaseInsensitiveOverride(t *testing.T) {
for _, tc := range []struct {
name string
params map[string]any
want map[string]string
wantError bool
}{
{
name: "provided parameter overrides static header",
params: map[string]any{"CONTENT-type": "application/json; charset=UTF-8"},
want: map[string]string{"Content-Type": "application/json; charset=UTF-8"},
},
{
name: "omitted parameter retains static header",
want: map[string]string{"Content-Type": "text/plain; charset=UTF-8"},
},
{
name: "nil parameter retains existing error behavior",
params: map[string]any{"CONTENT-type": nil},
wantError: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
headerParams := parameters.Parameters{parameters.NewStringParameter("CONTENT-type", "content type", parameters.WithStringRequired(false))}
got, err := getHeaders(headerParams, map[string]string{"content-type": "text/plain; charset=UTF-8"}, tc.params)
if (err != nil) != tc.wantError {
t.Fatalf("getHeaders() error = %v, wantError %v", err, tc.wantError)
}
if !tc.wantError && !maps.Equal(got, tc.want) {
t.Fatalf("getHeaders() = %v, want %v", got, tc.want)
}
})
}
}

func TestGetURLHostOverride(t *testing.T) {
testCases := []struct {
name string
Expand Down
Loading