From b42926132845f5e3ed3ee267f0b162b5dbb7c351 Mon Sep 17 00:00:00 2001 From: Carlos Alexandro Becker Date: Thu, 9 Oct 2025 11:36:00 -0300 Subject: [PATCH] refactor(mcp): use the new mcp library Signed-off-by: Carlos Alexandro Becker --- go.mod | 4 +- go.sum | 16 ++-- internal/config/config.go | 2 +- internal/llm/agent/mcp-tools.go | 159 ++++++++++++++++---------------- 4 files changed, 89 insertions(+), 92 deletions(-) diff --git a/go.mod b/go.mod index 170788928c44d7e233da6c25871927f3a8bf2073..843e7f231f729e86d4e299349fa1293005ad3971 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/google/uuid v1.6.0 github.com/invopop/jsonschema v0.13.0 github.com/joho/godotenv v1.5.1 - github.com/mark3labs/mcp-go v0.41.1 + github.com/modelcontextprotocol/go-sdk v1.0.0 github.com/muesli/termenv v0.16.0 github.com/ncruces/go-sqlite3 v0.29.1 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 @@ -91,6 +91,7 @@ require ( github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/jsonschema-go v0.3.0 // indirect github.com/google/s2a-go v0.1.8 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect github.com/googleapis/gax-go/v2 v2.14.1 // indirect @@ -121,7 +122,6 @@ require ( github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sourcegraph/jsonrpc2 v0.2.1 // indirect - github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.9 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect diff --git a/go.sum b/go.sum index 3669305d22b191791df373899305e5e18a4e1f71..563016cca9ffcec4a7be40aeed80822a105d1769 100644 --- a/go.sum +++ b/go.sum @@ -130,8 +130,6 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= @@ -144,13 +142,15 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -194,8 +194,6 @@ github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQ github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mark3labs/mcp-go v0.41.1 h1:w78eWfiQam2i8ICL7AL0WFiq7KHNJQ6UB53ZVtH4KGA= -github.com/mark3labs/mcp-go v0.41.1/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= @@ -206,6 +204,8 @@ github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwX github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modelcontextprotocol/go-sdk v1.0.0 h1:Z4MSjLi38bTgLrd/LjSmofqRqyBiVKRyQSJgw8q8V74= +github.com/modelcontextprotocol/go-sdk v1.0.0/go.mod h1:nYtYQroQ2KQiM0/SbyEPUWQ6xs4B95gJjEalc9AQyOs= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= @@ -265,8 +265,6 @@ github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/sourcegraph/jsonrpc2 v0.2.1 h1:2GtljixMQYUYCmIg7W9aF2dFmniq/mOr2T9tFRh6zSQ= github.com/sourcegraph/jsonrpc2 v0.2.1/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= @@ -422,6 +420,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.211.0 h1:IUpLjq09jxBSV1lACO33CGY3jsRcbctfGzhj+ZSE/Bg= google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0= diff --git a/internal/config/config.go b/internal/config/config.go index 858fa1c47b33f6a5e6bafb81b4799ea5739736f9..b37b98cad717e789ad16237b3ca250a2f1555ba9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -99,7 +99,7 @@ type MCPType string const ( MCPStdio MCPType = "stdio" - MCPSse MCPType = "sse" + MCPSSE MCPType = "sse" MCPHttp MCPType = "http" ) diff --git a/internal/llm/agent/mcp-tools.go b/internal/llm/agent/mcp-tools.go index 181f32b7280faf3eb36040d2ebecf3f892350f53..867047cfc825ddcab35d03ec202e29fe889cdade 100644 --- a/internal/llm/agent/mcp-tools.go +++ b/internal/llm/agent/mcp-tools.go @@ -8,6 +8,8 @@ import ( "fmt" "log/slog" "maps" + "net/http" + "os/exec" "strings" "sync" "time" @@ -19,9 +21,7 @@ import ( "github.com/charmbracelet/crush/internal/permission" "github.com/charmbracelet/crush/internal/pubsub" "github.com/charmbracelet/crush/internal/version" - "github.com/mark3labs/mcp-go/client" - "github.com/mark3labs/mcp-go/client/transport" - "github.com/mark3labs/mcp-go/mcp" + "github.com/modelcontextprotocol/go-sdk/mcp" ) // MCPState represents the current state of an MCP client @@ -71,7 +71,7 @@ type MCPClientInfo struct { Name string State MCPState Error error - Client *client.Client + Client *mcp.ClientSession ToolCount int ConnectedAt time.Time } @@ -80,14 +80,14 @@ var ( mcpToolsOnce sync.Once mcpTools = csync.NewMap[string, tools.BaseTool]() mcpClient2Tools = csync.NewMap[string, []tools.BaseTool]() - mcpClients = csync.NewMap[string, *client.Client]() + mcpClients = csync.NewMap[string, *mcp.ClientSession]() mcpStates = csync.NewMap[string, MCPClientInfo]() mcpBroker = pubsub.NewBroker[MCPEvent]() ) type McpTool struct { mcpName string - tool mcp.Tool + tool *mcp.Tool permissions permission.Service workingDir string } @@ -97,14 +97,9 @@ func (b *McpTool) Name() string { } func (b *McpTool) Info() tools.ToolInfo { - required := b.tool.InputSchema.Required - if required == nil { - required = make([]string, 0) - } - parameters := b.tool.InputSchema.Properties - if parameters == nil { - parameters = make(map[string]any) - } + input := b.tool.InputSchema.(map[string]any) + required, _ := input["required"].([]string) + parameters, _ := input["properties"].(map[string]any) return tools.ToolInfo{ Name: fmt.Sprintf("mcp_%s_%s", b.mcpName, b.tool.Name), Description: b.tool.Description, @@ -123,11 +118,9 @@ func runTool(ctx context.Context, name, toolName string, input string) (tools.To if err != nil { return tools.NewTextErrorResponse(err.Error()), nil } - result, err := c.CallTool(ctx, mcp.CallToolRequest{ - Params: mcp.CallToolParams{ - Name: toolName, - Arguments: args, - }, + result, err := c.CallTool(ctx, &mcp.CallToolParams{ + Name: toolName, + Arguments: args, }) if err != nil { return tools.NewTextErrorResponse(err.Error()), nil @@ -135,7 +128,7 @@ func runTool(ctx context.Context, name, toolName string, input string) (tools.To output := make([]string, 0, len(result.Content)) for _, v := range result.Content { - if v, ok := v.(mcp.TextContent); ok { + if v, ok := v.(*mcp.TextContent); ok { output = append(output, v.Text) } else { output = append(output, fmt.Sprintf("%v", v)) @@ -144,8 +137,8 @@ func runTool(ctx context.Context, name, toolName string, input string) (tools.To return tools.NewTextResponse(strings.Join(output, "\n")), nil } -func getOrRenewClient(ctx context.Context, name string) (*client.Client, error) { - c, ok := mcpClients.Get(name) +func getOrRenewClient(ctx context.Context, name string) (*mcp.ClientSession, error) { + sess, ok := mcpClients.Get(name) if !ok { return nil, fmt.Errorf("mcp '%s' not available", name) } @@ -157,20 +150,20 @@ func getOrRenewClient(ctx context.Context, name string) (*client.Client, error) timeout := mcpTimeout(m) pingCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() - err := c.Ping(pingCtx) + err := sess.Ping(pingCtx, nil) if err == nil { - return c, nil + return sess, nil } updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, state.ToolCount) - c, err = createAndInitializeClient(ctx, name, m, cfg.Resolver()) + sess, err = createMCPSession(ctx, name, m, cfg.Resolver()) if err != nil { return nil, err } - updateMCPState(name, MCPStateConnected, nil, c, state.ToolCount) - mcpClients.Set(name, c) - return c, nil + updateMCPState(name, MCPStateConnected, nil, sess, state.ToolCount) + mcpClients.Set(name, sess) + return sess, nil } func (b *McpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolResponse, error) { @@ -197,8 +190,8 @@ func (b *McpTool) Run(ctx context.Context, params tools.ToolCall) (tools.ToolRes return runTool(ctx, b.mcpName, b.tool.Name, params.Input) } -func getTools(ctx context.Context, name string, permissions permission.Service, c *client.Client, workingDir string) ([]tools.BaseTool, error) { - result, err := c.ListTools(ctx, mcp.ListToolsRequest{}) +func getTools(ctx context.Context, name string, permissions permission.Service, c *mcp.ClientSession, workingDir string) ([]tools.BaseTool, error) { + result, err := c.ListTools(ctx, &mcp.ListToolsParams{}) if err != nil { return nil, err } @@ -230,7 +223,7 @@ func GetMCPState(name string) (MCPClientInfo, bool) { } // updateMCPState updates the state of an MCP client and publishes an event -func updateMCPState(name string, state MCPState, err error, client *client.Client, toolCount int) { +func updateMCPState(name string, state MCPState, err error, client *mcp.ClientSession, toolCount int) { info := MCPClientInfo{ Name: name, State: state, @@ -277,16 +270,6 @@ func CloseMCPClients() error { return errors.Join(errs...) } -var mcpInitRequest = mcp.InitializeRequest{ - Params: mcp.InitializeParams{ - ProtocolVersion: mcp.LATEST_PROTOCOL_VERSION, - ClientInfo: mcp.Implementation{ - Name: "Crush", - Version: version.Version, - }, - }, -} - func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *config.Config) { var wg sync.WaitGroup // Initialize states for all configured MCPs @@ -322,7 +305,7 @@ func doGetMCPTools(ctx context.Context, permissions permission.Service, cfg *con ctx, cancel := context.WithTimeout(ctx, mcpTimeout(m)) defer cancel() - c, err := createAndInitializeClient(ctx, name, m, cfg.Resolver()) + c, err := createMCPSession(ctx, name, m, cfg.Resolver()) if err != nil { return } @@ -359,22 +342,25 @@ func updateMcpTools(mcpName string, tools []tools.BaseTool) { } } -func createAndInitializeClient(ctx context.Context, name string, m config.MCPConfig, resolver config.VariableResolver) (*client.Client, error) { - c, err := createMcpClient(name, m, resolver) +func createMCPSession(ctx context.Context, name string, m config.MCPConfig, resolver config.VariableResolver) (*mcp.ClientSession, error) { + transport, err := createMCPTransport(name, m, resolver) if err != nil { updateMCPState(name, MCPStateError, err, nil, 0) slog.Error("error creating mcp client", "error", err, "name", name) return nil, err } - c.OnNotification(func(n mcp.JSONRPCNotification) { - slog.Debug("Received MCP notification", "name", name, "notification", n) - switch n.Method { - case "notifications/tools/list_changed": - publishMCPEventToolsListChanged(name) - default: - slog.Debug("Unhandled MCP notification", "name", name, "method", n.Method) - } + client := mcp.NewClient(&mcp.Implementation{ + Name: "crush", + Version: version.Version, + Title: "Crush", + }, &mcp.ClientOptions{ + ToolListChangedHandler: func(context.Context, *mcp.ToolListChangedRequest) { + mcpBroker.Publish(pubsub.UpdatedEvent, MCPEvent{ + Type: MCPEventToolsListChanged, + Name: name, + }) + }, }) // XXX: ideally we should be able to use context.WithTimeout here, but, @@ -383,25 +369,18 @@ func createAndInitializeClient(ctx context.Context, name string, m config.MCPCon mcpCtx, cancel := context.WithCancel(ctx) cancelTimer := time.AfterFunc(timeout, cancel) - if err := c.Start(mcpCtx); err != nil { + session, err := client.Connect(mcpCtx, transport, nil) + if err != nil { updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, 0) slog.Error("error starting mcp client", "error", err, "name", name) - _ = c.Close() - cancel() - return nil, err - } - - if _, err := c.Initialize(mcpCtx, mcpInitRequest); err != nil { - updateMCPState(name, MCPStateError, maybeTimeoutErr(err, timeout), nil, 0) - slog.Error("error initializing mcp client", "error", err, "name", name) - _ = c.Close() + _ = session.Close() cancel() return nil, err } cancelTimer.Stop() slog.Info("Initialized mcp client", "name", name) - return c, nil + return session, nil } func maybeTimeoutErr(err error, timeout time.Duration) error { @@ -411,7 +390,7 @@ func maybeTimeoutErr(err error, timeout time.Duration) error { return err } -func createMcpClient(name string, m config.MCPConfig, resolver config.VariableResolver) (*client.Client, error) { +func createMCPTransport(name string, m config.MCPConfig, resolver config.VariableResolver) (mcp.Transport, error) { switch m.Type { case config.MCPStdio: command, err := resolver.ResolveValue(m.Command) @@ -421,35 +400,53 @@ func createMcpClient(name string, m config.MCPConfig, resolver config.VariableRe if strings.TrimSpace(command) == "" { return nil, fmt.Errorf("mcp stdio config requires a non-empty 'command' field") } - return client.NewStdioMCPClientWithOptions( - home.Long(command), - m.ResolvedEnv(), - m.Args, - transport.WithCommandLogger(mcpLogger{name: name}), - ) + cmd := exec.Command(home.Long(command), m.Args...) + cmd.Env = m.ResolvedEnv() + return &mcp.CommandTransport{ + Command: cmd, + }, nil case config.MCPHttp: if strings.TrimSpace(m.URL) == "" { return nil, fmt.Errorf("mcp http config requires a non-empty 'url' field") } - return client.NewStreamableHttpClient( - m.URL, - transport.WithHTTPHeaders(m.ResolvedHeaders()), - transport.WithHTTPLogger(mcpLogger{name: name}), - ) - case config.MCPSse: + client := &http.Client{ + Transport: &headerRoundTripper{ + headers: m.ResolvedHeaders(), + }, + } + return &mcp.StreamableClientTransport{ + Endpoint: m.URL, + HTTPClient: client, + }, nil + case config.MCPSSE: if strings.TrimSpace(m.URL) == "" { return nil, fmt.Errorf("mcp sse config requires a non-empty 'url' field") } - return client.NewSSEMCPClient( - m.URL, - client.WithHeaders(m.ResolvedHeaders()), - transport.WithSSELogger(mcpLogger{name: name}), - ) + client := &http.Client{ + Transport: &headerRoundTripper{ + headers: m.ResolvedHeaders(), + }, + } + return &mcp.SSEClientTransport{ + Endpoint: m.URL, + HTTPClient: client, + }, nil default: return nil, fmt.Errorf("unsupported mcp type: %s", m.Type) } } +type headerRoundTripper struct { + headers map[string]string +} + +func (rt headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + for k, v := range rt.headers { + req.Header.Set(k, v) + } + return http.DefaultTransport.RoundTrip(req) +} + // for MCP's clients. type mcpLogger struct{ name string }