Skip to content

Commit ae753e7

Browse files
authored
remove the --remote flag while running remote server for better user experience (#1517)
1 parent 0cd8707 commit ae753e7

File tree

4 files changed

+93
-22
lines changed

4 files changed

+93
-22
lines changed

cmd/thv/app/run.go

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/url"
99
"os"
1010
"os/signal"
11+
"strings"
1112
"syscall"
1213
"time"
1314

@@ -17,6 +18,7 @@ import (
1718
"github.com/stacklok/toolhive/pkg/container/runtime"
1819
"github.com/stacklok/toolhive/pkg/groups"
1920
"github.com/stacklok/toolhive/pkg/logger"
21+
"github.com/stacklok/toolhive/pkg/networking"
2022
"github.com/stacklok/toolhive/pkg/process"
2123
"github.com/stacklok/toolhive/pkg/runner"
2224
"github.com/stacklok/toolhive/pkg/workloads"
@@ -62,7 +64,7 @@ ToolHive supports five ways to run an MCP server:
6264
6365
5. Remote MCP server:
6466
65-
$ thv run --remote <URL> [--name <name>]
67+
$ thv run <URL> [--name <name>]
6668
6769
Runs a remote MCP server as a workload, proxying requests to the specified URL.
6870
This allows remote MCP servers to be managed like local workloads with full
@@ -75,10 +77,6 @@ permission profile. Additional configuration can be provided via flags.`,
7577
if runFlags.FromConfig != "" {
7678
return nil
7779
}
78-
// If --remote is provided, no args are required
79-
if runFlags.RemoteURL != "" {
80-
return nil
81-
}
8280
// Otherwise, require at least 1 argument
8381
return cobra.MinimumNArgs(1)(cmd, args)
8482
},
@@ -127,6 +125,7 @@ func cleanupAndWait(workloadManager workloads.Manager, name string, cancel conte
127125
}
128126
}
129127

128+
// nolint:gocyclo // This function is complex by design
130129
func runCmdFunc(cmd *cobra.Command, args []string) error {
131130
ctx := cmd.Context()
132131

@@ -136,21 +135,23 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
136135
}
137136

138137
// Get the name of the MCP server to run.
139-
// This may be a server name from the registry, a container image, or a protocol scheme.
140-
// When using --from-config or --remote, no args are required
138+
// This may be a server name from the registry, a container image, a protocol scheme, or a remote URL.
141139
var serverOrImage string
142140
if len(args) > 0 {
143141
serverOrImage = args[0]
144142
}
145143

146-
// If --remote is provided but no name is given, generate a name from the URL
147-
if runFlags.RemoteURL != "" && runFlags.Name == "" {
148-
// Extract a name from the remote URL
149-
name, err := deriveRemoteName()
150-
if err != nil {
151-
return err
144+
// Check if the server name is actually a URL (remote server)
145+
if serverOrImage != "" && networking.IsURL(serverOrImage) {
146+
runFlags.RemoteURL = serverOrImage
147+
// If no name is given, generate a name from the URL
148+
if runFlags.Name == "" {
149+
name, err := deriveRemoteName(serverOrImage)
150+
if err != nil {
151+
return err
152+
}
153+
runFlags.Name = name
152154
}
153-
runFlags.Name = name
154155
}
155156

156157
// Process command arguments using os.Args to find everything after --
@@ -205,17 +206,26 @@ func runCmdFunc(cmd *cobra.Command, args []string) error {
205206
return workloadManager.RunWorkloadDetached(ctx, runnerConfig)
206207
}
207208

208-
func deriveRemoteName() (string, error) {
209-
parsedURL, err := url.Parse(runFlags.RemoteURL)
209+
// deriveRemoteName extracts a name from a remote URL
210+
func deriveRemoteName(remoteURL string) (string, error) {
211+
parsedURL, err := url.Parse(remoteURL)
210212
if err != nil {
211-
return "", fmt.Errorf("invalid remote URL: %v", err)
213+
return "", fmt.Errorf("invalid remote URL: %w", err)
212214
}
215+
213216
// Use the hostname as the base name
214217
hostname := parsedURL.Hostname()
215218
if hostname == "" {
216-
hostname = "remote"
219+
return "", fmt.Errorf("could not extract hostname from URL: %s", remoteURL)
217220
}
218-
return fmt.Sprintf("%s-remote", hostname), nil
221+
222+
// Remove common TLDs and use the main domain name
223+
parts := strings.Split(hostname, ".")
224+
if len(parts) >= 2 {
225+
return parts[len(parts)-2], nil
226+
}
227+
228+
return hostname, nil
219229
}
220230

221231
func runForeground(ctx context.Context, workloadManager workloads.Manager, runnerConfig *runner.RunConfig) error {

cmd/thv/app/run_flags.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ func AddRunFlags(cmd *cobra.Command, config *RunFlags) {
146146
[]string{},
147147
"Specify a secret to be fetched from the secrets manager and set as an environment variable (format: NAME,target=TARGET)",
148148
)
149-
cmd.Flags().StringVar(&config.RemoteURL, "remote", "", "URL of remote MCP server to run as a workload")
150149
cmd.Flags().StringVar(&config.AuthzConfig, "authz-config", "", "Path to the authorization configuration file")
151150
cmd.Flags().StringVar(&config.AuditConfig, "audit-config", "", "Path to the audit configuration file")
152151
cmd.Flags().BoolVar(&config.EnableAudit, "enable-audit", false, "Enable audit logging with default configuration")

cmd/thv/app/run_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package app
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestDeriveRemoteName(t *testing.T) {
8+
t.Parallel()
9+
tests := []struct {
10+
name string
11+
url string
12+
expected string
13+
wantErr bool
14+
}{
15+
{
16+
name: "api.github.com should return github",
17+
url: "https://api.github.com",
18+
expected: "github",
19+
wantErr: false,
20+
},
21+
{
22+
name: "github.com should return github",
23+
url: "https://github.com",
24+
expected: "github",
25+
wantErr: false,
26+
},
27+
{
28+
name: "invalid URL should return error",
29+
url: "not-a-url",
30+
expected: "",
31+
wantErr: true,
32+
},
33+
{
34+
name: "empty URL should return error",
35+
url: "",
36+
expected: "",
37+
wantErr: true,
38+
},
39+
}
40+
41+
for _, tt := range tests {
42+
t.Run(tt.name, func(t *testing.T) {
43+
t.Parallel()
44+
got, err := deriveRemoteName(tt.url)
45+
46+
if tt.wantErr {
47+
if err == nil {
48+
t.Errorf("deriveRemoteName() expected error but got none")
49+
}
50+
return
51+
}
52+
53+
if err != nil {
54+
t.Errorf("deriveRemoteName() unexpected error: %v", err)
55+
return
56+
}
57+
58+
if got != tt.expected {
59+
t.Errorf("deriveRemoteName() = %v, want %v", got, tt.expected)
60+
}
61+
})
62+
}
63+
}

docs/cli/thv_run.md

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

0 commit comments

Comments
 (0)