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
2 changes: 1 addition & 1 deletion docs/cli/tiger_mcp_start_http.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 6 additions & 30 deletions internal/cmd/mcp_start_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log/slog"
"net"
"net/http"
"strconv"

"github.com/spf13/cobra"

Expand All @@ -24,7 +25,7 @@ func buildMCPHTTPCmd(app *common.App) *cobra.Command {
Short: "Start MCP server with HTTP transport",
Long: `Start the MCP server using HTTP transport.

The server will automatically find an available port if the specified port is busy.`,
The server listens on the given port and fails if that port is already in use.`,
Example: ` # Start HTTP server on default port 8080
tiger mcp start http

Expand Down Expand Up @@ -64,27 +65,14 @@ func startHTTPServer(cmd *cobra.Command, app *common.App, host string, port int)
}
defer server.Close()

// Find available port and get the listener
listener, actualPort, err := getListener(host, port)
address := net.JoinHostPort(host, strconv.Itoa(port))
listener, err := net.Listen("tcp", address)
if err != nil {
logger.Error("Failed to get listener",
slog.String("host", host),
slog.Int("port", port),
slog.Any("error", err),
)
return fmt.Errorf("failed to get listener: %w", err)
logger.Error("Failed to listen", slog.Any("error", err))
return fmt.Errorf("failed to listen: %w", err)
}
defer listener.Close()

if actualPort != port {
logger.Info("Specified port was busy, using alternative port",
slog.Int("requested_port", port),
slog.Int("actual_port", actualPort),
)
}

address := fmt.Sprintf("%s:%d", host, actualPort)

// Create HTTP server
httpServer := &http.Server{
Handler: server.HTTPHandler(),
Expand Down Expand Up @@ -128,15 +116,3 @@ func startHTTPServer(cmd *cobra.Command, app *common.App, host string, port int)
}
return nil
}

// getListener finds an available port starting from the specified port and returns the listener
func getListener(host string, startPort int) (net.Listener, int, error) {
for port := startPort; port < startPort+100; port++ {
address := fmt.Sprintf("%s:%d", host, port)
listener, err := net.Listen("tcp", address)
if err == nil {
return listener, port, nil
}
}
return nil, 0, fmt.Errorf("no available port found in range %d-%d", startPort, startPort+99)
}
28 changes: 7 additions & 21 deletions internal/cmd/mcp_start_http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
)

// freePort returns a port nothing is listening on, and (when hold is true)
// keeps it occupied for the rest of the test so the command has to fall back.
// keeps it occupied for the rest of the test so binding it fails.
func freePort(t *testing.T, hold bool) int {
t.Helper()
listener, err := net.Listen("tcp", "localhost:0")
Expand Down Expand Up @@ -56,27 +56,13 @@ INFO Tiger MCP server started address=localhost:%d
`, port) + shutdown),
},
{
// The port scan walks forward from the requested one. Which port
// it lands on depends on what else is running, so only the
// fallback itself is asserted.
name: "falls back to another port when the requested one is busy",
args: []string{"mcp", "start", "http", "--port", fmt.Sprint(busyPort)},
opts: append(noDocsProxy(nil), withContext(cancelled)),
wantStderr: matchLogPort(fmt.Sprintf(`INFO Docs MCP proxy is disabled
INFO Specified port was busy, using alternative port requested_port=%d actual_port=<port>
INFO Tiger MCP server started address=localhost:<port>
`, busyPort) + shutdown),
},
{
// 192.0.2.1 is TEST-NET-1: it resolves to nothing local, so
// every port in the scan range fails to bind.
name: "reports when no port in range is available",
args: []string{"mcp", "start", "http", "--host", "192.0.2.1"},
name: "fails when the requested port is busy",
args: []string{"mcp", "start", "http", "--port", fmt.Sprint(busyPort)},
opts: append(noDocsProxy(nil), withContext(cancelled)),
wantErr: "failed to get listener: no available port found in range 8080-8179",
wantStderr: matchLog(`INFO Docs MCP proxy is disabled
ERROR Failed to get listener host=192.0.2.1 port=8080 error="no available port found in range 8080-8179"
`),
wantErr: fmt.Sprintf("failed to listen: listen tcp 127.0.0.1:%d: bind: address already in use", busyPort),
wantStderr: matchLog(fmt.Sprintf(`INFO Docs MCP proxy is disabled
ERROR Failed to listen error="listen tcp 127.0.0.1:%d: bind: address already in use"
`, busyPort)),
},
{
name: "rejects positional args",
Expand Down
14 changes: 0 additions & 14 deletions internal/cmd/mcp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package cmd
import (
"maps"
"regexp"
"strings"
"testing"
)

Expand Down Expand Up @@ -31,16 +30,3 @@ func matchLog(want string) matcher {
assertOutput(t, logTimestamp.ReplaceAllString(got, ""), want)
})
}

// matchLogPort is matchLog for output naming a port the OS picked rather than
// one the test chose. The want text is matched literally except for "<port>",
// which stands in for any port number.
func matchLogPort(want string) matcher {
re := regexp.MustCompile("^" + strings.ReplaceAll(regexp.QuoteMeta(want), "<port>", `\d+`) + "$")
return matchFunc(func(t *testing.T, got string) {
t.Helper()
if stripped := logTimestamp.ReplaceAllString(got, ""); !re.MatchString(stripped) {
t.Errorf("log = %q, want %q (<port> matching any port)", stripped, want)
}
})
}
Loading