Skip to content
Open
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
1 change: 1 addition & 0 deletions cmd/thv-operator/controllers/mcpserver_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ func (r *MCPServerReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
host := fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, mcpServer.Namespace)
mcpServer.Status.URL = transport.GenerateMCPServerURL(
mcpServer.Spec.Transport,
mcpServer.Spec.ProxyMode,
host,
int(mcpServer.GetProxyPort()),
mcpServer.Name,
Expand Down
1 change: 1 addition & 0 deletions pkg/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ func (r *Runner) Run(ctx context.Context) error {
transportType := labels.GetTransportType(r.Config.ContainerLabels)
serverURL := transport.GenerateMCPServerURL(
transportType,
string(r.Config.ProxyMode),
"localhost",
r.Config.Port,
r.Config.ContainerName,
Expand Down
28 changes: 25 additions & 3 deletions pkg/transport/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,33 @@ import (
// If remoteURL is provided, the remote server's path will be used as the path of the proxy.
// For SSE/STDIO transports, a "#<containerName>" fragment is appended.
// For StreamableHTTP, no fragment is appended.
func GenerateMCPServerURL(transportType string, host string, port int, containerName, remoteURL string) string {
func GenerateMCPServerURL(transportType string, proxyMode string, host string, port int, containerName, remoteURL string) string {
base := fmt.Sprintf("http://%s:%d", host, port)

isSSE := transportType == types.TransportTypeSSE.String() || transportType == types.TransportTypeStdio.String()
isStreamable := transportType == types.TransportTypeStreamableHTTP.String()
var isSSE, isStreamable bool

if transportType == types.TransportTypeStdio.String() {
// For stdio, the proxy mode determines the HTTP endpoint
// Default to streamable-http if proxyMode is empty (matches CRD default)
effectiveProxyMode := proxyMode
if effectiveProxyMode == "" {
effectiveProxyMode = types.ProxyModeStreamableHTTP.String()
}

// Map proxy mode to endpoint type
if effectiveProxyMode == types.ProxyModeSSE.String() {
isSSE = true
} else {
// streamable-http or any other value
isStreamable = true
}
} else if transportType == types.TransportTypeSSE.String() {
// Native SSE transport
isSSE = true
} else if transportType == types.TransportTypeStreamableHTTP.String() {
// Native streamable-http transport
isStreamable = true
}

// ---- Remote path case ----
if remoteURL != "" {
Expand Down
61 changes: 56 additions & 5 deletions pkg/transport/url_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,48 @@ func TestGenerateMCPServerURL(t *testing.T) {
tests := []struct {
name string
transportType string
proxyMode string
host string
port int
containerName string
targetURI string
expected string
}{
{
name: "SSE transport",
transportType: types.TransportTypeSSE.String(),
name: "STDIO transport with streamable-http proxy",
transportType: types.TransportTypeStdio.String(),
proxyMode: "streamable-http",
host: "localhost",
port: 12345,
containerName: "test-container",
targetURI: "",
expected: "http://localhost:12345/" + streamable.HTTPStreamableHTTPEndpoint,
},
{
name: "STDIO transport with sse proxy",
transportType: types.TransportTypeStdio.String(),
proxyMode: "sse",
host: "localhost",
port: 12345,
containerName: "test-container",
targetURI: "",
expected: "http://localhost:12345" + ssecommon.HTTPSSEEndpoint + "#test-container",
},
{
name: "STDIO transport (uses SSE proxy)",
name: "STDIO transport with empty proxyMode (defaults to streamable-http)",
transportType: types.TransportTypeStdio.String(),

proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
targetURI: "",
expected: "http://localhost:12345/" + streamable.HTTPStreamableHTTPEndpoint,
},
{
name: "SSE transport",
transportType: types.TransportTypeSSE.String(),
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
Expand All @@ -41,6 +65,7 @@ func TestGenerateMCPServerURL(t *testing.T) {
{
name: "Streamable HTTP transport",
transportType: types.TransportTypeStreamableHTTP.String(),
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
Expand All @@ -50,6 +75,7 @@ func TestGenerateMCPServerURL(t *testing.T) {
{
name: "Unsupported transport type",
transportType: "unsupported",
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
Expand All @@ -59,6 +85,7 @@ func TestGenerateMCPServerURL(t *testing.T) {
{
name: "SSE transport with targetURI path",
transportType: types.TransportTypeSSE.String(),
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
Expand All @@ -68,6 +95,7 @@ func TestGenerateMCPServerURL(t *testing.T) {
{
name: "SSE transport with targetURI domain only",
transportType: types.TransportTypeSSE.String(),
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
Expand All @@ -77,16 +105,17 @@ func TestGenerateMCPServerURL(t *testing.T) {
{
name: "SSE transport with targetURI root path",
transportType: types.TransportTypeSSE.String(),
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
targetURI: "http://example.com/",
expected: "http://localhost:12345#test-container",
},
// Major targetURI test cases - Streamable HTTP transport
{
name: "Streamable HTTP transport with targetURI path",
transportType: types.TransportTypeStreamableHTTP.String(),
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
Expand All @@ -96,6 +125,7 @@ func TestGenerateMCPServerURL(t *testing.T) {
{
name: "Streamable HTTP transport with targetURI domain only",
transportType: types.TransportTypeStreamableHTTP.String(),
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
Expand All @@ -105,18 +135,39 @@ func TestGenerateMCPServerURL(t *testing.T) {
{
name: "Streamable HTTP transport with targetURI root path",
transportType: types.TransportTypeStreamableHTTP.String(),
proxyMode: "",
host: "localhost",
port: 12345,
containerName: "test-container",
targetURI: "http://remote-server.com/",
expected: "http://localhost:12345",
},
{
name: "STDIO with streamable-http proxy and targetURI",
transportType: types.TransportTypeStdio.String(),
proxyMode: "streamable-http",
host: "localhost",
port: 12345,
containerName: "test-container",
targetURI: "http://remote.com/api",
expected: "http://localhost:12345/api",
},
{
name: "STDIO with sse proxy and targetURI",
transportType: types.TransportTypeStdio.String(),
proxyMode: "sse",
host: "localhost",
port: 12345,
containerName: "test-container",
targetURI: "http://remote.com/api",
expected: "http://localhost:12345/api#test-container",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
url := GenerateMCPServerURL(tt.transportType, tt.host, tt.port, tt.containerName, tt.targetURI)
url := GenerateMCPServerURL(tt.transportType, tt.proxyMode, tt.host, tt.port, tt.containerName, tt.targetURI)
if url != tt.expected {
t.Errorf("GenerateMCPServerURL() = %v, want %v", url, tt.expected)
}
Expand Down
9 changes: 8 additions & 1 deletion pkg/vmcp/workloads/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,14 @@ func (d *k8sDiscoverer) mcpServerToBackend(ctx context.Context, mcpServer *mcpv1
port = int(mcpServer.Spec.Port) // Fallback to deprecated Port field
}
if port > 0 {
url = transport.GenerateMCPServerURL(mcpServer.Spec.Transport, transport.LocalhostIPv4, port, mcpServer.Name, "")
url = transport.GenerateMCPServerURL(
mcpServer.Spec.Transport,
mcpServer.Spec.ProxyMode,
transport.LocalhostIPv4,
port,
mcpServer.Name,
"",
)
}
}

Expand Down
1 change: 1 addition & 0 deletions pkg/workloads/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -1391,6 +1391,7 @@ func (d *DefaultManager) getRemoteWorkloadsFromState(
if runConfig.Port > 0 {
proxyURL = transport.GenerateMCPServerURL(
transportType.String(),
string(runConfig.ProxyMode),
transport.LocalhostIPv4,
runConfig.Port,
name,
Expand Down
23 changes: 11 additions & 12 deletions pkg/workloads/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,18 +69,23 @@ func WorkloadFromContainerInfo(container *runtime.ContainerInfo) (core.Workload,
// check if we have the label for transport type (toolhive-transport)
transportType := labels.GetTransportType(container.Labels)

// Generate URL for the MCP server
url := ""
if port > 0 {
url = transport.GenerateMCPServerURL(transportType, transport.LocalhostIPv4, port, name, "")
}

tType, err := types.ParseTransportType(transportType)
if err != nil {
// If we can't parse the transport type, default to SSE.
tType = types.TransportTypeSSE
}

ctx := context.Background()
runConfig, err := loadRunConfigFields(ctx, name)
if err != nil {
return core.Workload{}, err
}

// Generate URL for the MCP server
url := ""
if port > 0 {
url = transport.GenerateMCPServerURL(transportType, runConfig.ProxyMode, transport.LocalhostIPv4, port, name, "")
}
// Filter out standard ToolHive labels to show only user-defined labels
userLabels := make(map[string]string)
for key, value := range container.Labels {
Expand All @@ -89,12 +94,6 @@ func WorkloadFromContainerInfo(container *runtime.ContainerInfo) (core.Workload,
}
}

ctx := context.Background()
runConfig, err := loadRunConfigFields(ctx, name)
if err != nil {
return core.Workload{}, err
}

// Calculate the effective proxy mode that clients should use
effectiveProxyMode := GetEffectiveProxyMode(tType, runConfig.ProxyMode)

Expand Down
Loading