Skip to content

Commit 45dc49f

Browse files
authored
Add streamable HTTP support to MCP client with auto-detection and transport flag (#1175)
Signed-off-by: Juan Antonio Osorio <[email protected]>
1 parent 3a7feb5 commit 45dc49f

File tree

5 files changed

+69
-6
lines changed

5 files changed

+69
-6
lines changed

cmd/thv/app/mcp.go

Lines changed: 65 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"net/url"
78
"os"
9+
"strings"
810
"text/tabwriter"
911
"time"
1012

@@ -13,13 +15,17 @@ import (
1315
"github.com/spf13/cobra"
1416

1517
"github.com/stacklok/toolhive/pkg/logger"
18+
"github.com/stacklok/toolhive/pkg/transport/ssecommon"
19+
"github.com/stacklok/toolhive/pkg/transport/streamable"
20+
"github.com/stacklok/toolhive/pkg/transport/types"
1621
"github.com/stacklok/toolhive/pkg/versions"
1722
)
1823

1924
var (
2025
mcpServerURL string
2126
mcpFormat string
2227
mcpTimeout time.Duration
28+
mcpTransport string
2329
)
2430

2531
func newMCPCommand() *cobra.Command {
@@ -80,6 +86,7 @@ func addMCPFlags(cmd *cobra.Command) {
8086
cmd.Flags().StringVar(&mcpServerURL, "server", "", "MCP server URL (required)")
8187
cmd.Flags().StringVar(&mcpFormat, "format", FormatText, "Output format (json or text)")
8288
cmd.Flags().DurationVar(&mcpTimeout, "timeout", 30*time.Second, "Connection timeout")
89+
cmd.Flags().StringVar(&mcpTransport, "transport", "auto", "Transport type (auto, sse, streamable-http)")
8390
_ = cmd.MarkFlagRequired("server")
8491
}
8592

@@ -197,15 +204,67 @@ func mcpListPromptsCmdFunc(cmd *cobra.Command, _ []string) error {
197204
return outputMCPData(map[string]interface{}{"prompts": result.Prompts}, mcpFormat)
198205
}
199206

200-
// createMCPClient creates an MCP client based on the server URL
207+
// createMCPClient creates an MCP client based on the server URL and transport type
201208
func createMCPClient() (*client.Client, error) {
202-
// For now, we'll use SSE client as the default
203-
// In the future, we could auto-detect or allow specifying the transport type
204-
mcpClient, err := client.NewSSEMCPClient(mcpServerURL)
209+
transportType := determineTransportType(mcpServerURL, mcpTransport)
210+
211+
switch transportType {
212+
case types.TransportTypeSSE:
213+
mcpClient, err := client.NewSSEMCPClient(mcpServerURL)
214+
if err != nil {
215+
return nil, fmt.Errorf("failed to create SSE MCP client: %w", err)
216+
}
217+
return mcpClient, nil
218+
case types.TransportTypeStreamableHTTP:
219+
mcpClient, err := client.NewStreamableHttpClient(mcpServerURL)
220+
if err != nil {
221+
return nil, fmt.Errorf("failed to create Streamable HTTP MCP client: %w", err)
222+
}
223+
return mcpClient, nil
224+
case types.TransportTypeStdio:
225+
return nil, fmt.Errorf("stdio transport is not supported for MCP client connections")
226+
case types.TransportTypeInspector:
227+
return nil, fmt.Errorf("inspector transport is not supported for MCP client connections")
228+
default:
229+
return nil, fmt.Errorf("unsupported transport type: %s", transportType)
230+
}
231+
}
232+
233+
// determineTransportType determines the transport type based on URL path and user preference
234+
func determineTransportType(serverURL, transportFlag string) types.TransportType {
235+
// If user explicitly specified a transport type, use it (unless it's "auto")
236+
if transportFlag != "auto" {
237+
switch transportFlag {
238+
case string(types.TransportTypeSSE):
239+
return types.TransportTypeSSE
240+
case string(types.TransportTypeStreamableHTTP):
241+
return types.TransportTypeStreamableHTTP
242+
}
243+
}
244+
245+
// Auto-detect based on URL path
246+
parsedURL, err := url.Parse(serverURL)
205247
if err != nil {
206-
return nil, fmt.Errorf("failed to create MCP client: %w", err)
248+
// If we can't parse the URL, default to SSE for backward compatibility
249+
logger.Warnf("Failed to parse server URL %s, defaulting to SSE transport: %v", serverURL, err)
250+
return types.TransportTypeSSE
207251
}
208-
return mcpClient, nil
252+
253+
path := parsedURL.Path
254+
255+
// Check for streamable HTTP endpoint (/mcp)
256+
if strings.HasSuffix(path, "/"+streamable.HTTPStreamableHTTPEndpoint) ||
257+
strings.HasSuffix(path, streamable.HTTPStreamableHTTPEndpoint) {
258+
return types.TransportTypeStreamableHTTP
259+
}
260+
261+
// Check for SSE endpoint (/sse)
262+
if strings.HasSuffix(path, ssecommon.HTTPSSEEndpoint) {
263+
return types.TransportTypeSSE
264+
}
265+
266+
// Default to SSE for backward compatibility
267+
return types.TransportTypeSSE
209268
}
210269

211270
// initializeMCPClient initializes the MCP client connection

docs/cli/thv_mcp_list.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/thv_mcp_list_prompts.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/thv_mcp_list_resources.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/cli/thv_mcp_list_tools.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)