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
8 changes: 4 additions & 4 deletions internal/tiger/cmd/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,7 +442,7 @@ Note: You can specify both CPU and memory together, or specify only one (the oth
} else {
// Wait for service to be ready
fmt.Fprintf(statusOutput, "⏳ Waiting for service to be ready (wait timeout: %v)...\n", createWaitTimeout)
service.Status, result = waitForServiceReady(client, projectID, serviceID, createWaitTimeout, statusOutput)
service.Status, result = waitForServiceReady(client, projectID, serviceID, createWaitTimeout, service.Status, statusOutput)
if result != nil {
fmt.Fprintf(statusOutput, "❌ %v\n", result)
} else {
Expand Down Expand Up @@ -841,14 +841,14 @@ func formatTimePtr(t *time.Time) string {
}

// waitForServiceReady polls the service status until it's ready or timeout occurs
func waitForServiceReady(client *api.ClientWithResponses, projectID, serviceID string, waitTimeout time.Duration, output io.Writer) (*api.DeployStatus, error) {
func waitForServiceReady(client *api.ClientWithResponses, projectID, serviceID string, waitTimeout time.Duration, initialStatus *api.DeployStatus, output io.Writer) (*api.DeployStatus, error) {
Copy link
Member Author

@nathanjcochran nathanjcochran Oct 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a small bug I noticed when in the command version of waitForServiceReady, where the lastStatus returned from the command would be nil if it timed out before the first poll. Unlikely in practice, but technically possible if the --wait-timeout is less than 10 seconds (our polling interval).

The fix is just to pass in the service's initial status (from the create/fork request), and set the initial value of lastStatus to that.

ctx, cancel := context.WithTimeout(context.Background(), waitTimeout)
defer cancel()

ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

var lastStatus *api.DeployStatus
lastStatus := initialStatus
for {
select {
case <-ctx.Done():
Expand Down Expand Up @@ -1312,7 +1312,7 @@ Examples:
} else {
// Wait for service to be ready
fmt.Fprintf(statusOutput, "⏳ Waiting for fork to complete (timeout: %v)...\n", forkWaitTimeout)
forkedService.Status, result = waitForServiceReady(client, projectID, forkedServiceID, forkWaitTimeout, statusOutput)
forkedService.Status, result = waitForServiceReady(client, projectID, forkedServiceID, forkWaitTimeout, forkedService.Status, statusOutput)
if result != nil {
fmt.Fprintf(statusOutput, "❌ %v\n", result)
} else {
Expand Down
2 changes: 1 addition & 1 deletion internal/tiger/cmd/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1252,7 +1252,7 @@ func TestWaitForServiceReady_Timeout(t *testing.T) {
cmd.SetErr(errBuf)

// Test waitForServiceReady with very short timeout to trigger timeout quickly
_, err = waitForServiceReady(client, "test-project-123", "svc-12345", 100*time.Millisecond, cmd.ErrOrStderr())
_, err = waitForServiceReady(client, "test-project-123", "svc-12345", 100*time.Millisecond, nil, cmd.ErrOrStderr())

// Should return an error with ExitTimeout
if err == nil {
Expand Down
5 changes: 2 additions & 3 deletions internal/tiger/mcp/service_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -507,7 +507,6 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque

service := *resp.JSON202
serviceID := util.Deref(service.ServiceId)
serviceStatus := util.DerefStr(service.Status)

output := ServiceCreateOutput{
Service: s.convertToServiceDetail(service),
Expand Down Expand Up @@ -558,10 +557,10 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque
if input.Wait {
timeout := time.Duration(*input.TimeoutMinutes) * time.Minute

output.Service, err = s.waitForServiceReady(apiClient, cfg.ProjectID, serviceID, timeout, serviceStatus)
if err != nil {
if status, err := s.waitForServiceReady(apiClient, cfg.ProjectID, serviceID, timeout, service.Status); err != nil {
output.Message = fmt.Sprintf("Error: %s", err.Error())
} else {
output.Service.Status = util.DerefStr(status)
Comment on lines -561 to +563
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the meat of the fix. Rather than overwriting output.Service with the latest polling result, we just overwrite output.Service.Status now (ensuring we don't overwrite the connection string/password set above).

output.Message = "Service created successfully and is ready!"
}
}
Expand Down
18 changes: 10 additions & 8 deletions internal/tiger/mcp/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ func (s *Server) convertToServiceDetail(service api.Service) ServiceDetail {

// waitForServiceReady polls the service status until it's ready or timeout occurs
// Returns the final ServiceDetail with current state and any error that occurred
func (s *Server) waitForServiceReady(apiClient *api.ClientWithResponses, projectID, serviceID string, timeout time.Duration, initialStatus string) (ServiceDetail, error) {
func (s *Server) waitForServiceReady(apiClient *api.ClientWithResponses, projectID, serviceID string, timeout time.Duration, initialStatus *api.DeployStatus) (*api.DeployStatus, error) {
logging.Debug("MCP: Waiting for service to be ready",
zap.String("service_id", serviceID),
zap.Duration("timeout", timeout),
Expand All @@ -140,12 +140,12 @@ func (s *Server) waitForServiceReady(apiClient *api.ClientWithResponses, project
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()

var lastService ServiceDetail
lastStatus := initialStatus
for {
select {
case <-ctx.Done():
logging.Warn("MCP: Timed out while waiting for service to be ready", zap.Error(ctx.Err()))
return lastService, fmt.Errorf("timeout reached after %v - service may still be provisioning", timeout)
return lastStatus, fmt.Errorf("timeout reached after %v - service may still be provisioning", timeout)
case <-ticker.C:
resp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, serviceID)
if err != nil {
Expand All @@ -158,18 +158,20 @@ func (s *Server) waitForServiceReady(apiClient *api.ClientWithResponses, project
continue
}

lastService = s.convertToServiceDetail(*resp.JSON200)
service := *resp.JSON200
lastStatus = service.Status
status := util.DerefStr(service.Status)

switch lastService.Status {
switch status {
case "READY":
logging.Debug("MCP: Service is ready", zap.String("service_id", serviceID))
return lastService, nil
return service.Status, nil
case "FAILED", "ERROR":
return lastService, fmt.Errorf("service creation failed with status: %s", lastService.Status)
return service.Status, fmt.Errorf("service creation failed with status: %s", status)
default:
logging.Debug("MCP: Service status",
zap.String("service_id", serviceID),
zap.String("status", lastService.Status),
zap.String("status", status),
)
}
}
Expand Down