-
Notifications
You must be signed in to change notification settings - Fork 6
Make tiger_service_create MCP tool set default service ID
#41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -84,18 +84,18 @@ func (s *Server) createAPIClient() (*api.ClientWithResponses, error) { | |
| return apiClient, nil | ||
| } | ||
|
|
||
| // loadProjectID loads fresh config and returns the current project ID | ||
| func (s *Server) loadProjectID() (string, error) { | ||
| // loadConfigWithProjectID loads fresh config and validates that project ID is set | ||
| func (s *Server) loadConfigWithProjectID() (*config.Config, error) { | ||
| // Load fresh config | ||
| cfg, err := config.Load() | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to load config: %w", err) | ||
| return nil, fmt.Errorf("failed to load config: %w", err) | ||
| } | ||
|
|
||
| if cfg.ProjectID == "" { | ||
| return "", fmt.Errorf("project ID is required. Please run 'tiger auth login' with --project-id") | ||
| return nil, fmt.Errorf("project ID is required. Please run 'tiger auth login' with --project-id") | ||
| } | ||
| return cfg.ProjectID, nil | ||
| return cfg, nil | ||
|
Comment on lines
-87
to
+98
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated this to return the full config instead of just the project ID (while still validating that the project ID is set).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is weird, and probably unnecessary. Why is it attached to
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is called at the start of every MCP tool call, and it does encapsulate the common projectID validation and error messages so they aren't repeated every time, which seems worthwhile to me. If anything, there's probably more shared logic that we could encapsulate in this function (though the name and return type would probably need to change).
You're right that it currently doesn't need to have a receiver, but it also doesn't really bother me, fwiw. Lots of people do this as a way of organizing/namespacing functions within a package. I think long-term, it probably makes sense to be related to the |
||
| } | ||
|
|
||
| // Close gracefully shuts down the MCP server and all proxy connections | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -82,14 +82,15 @@ type ServiceDetail struct { | |
|
|
||
| // ServiceCreateInput represents input for tiger_service_create | ||
| type ServiceCreateInput struct { | ||
| Name string `json:"name,omitempty"` | ||
| Addons []string `json:"addons,omitempty"` | ||
| Region string `json:"region,omitempty"` | ||
| CPUMemory string `json:"cpu_memory,omitempty"` | ||
| Replicas int `json:"replicas,omitempty"` | ||
| Free bool `json:"free,omitempty"` | ||
| Wait bool `json:"wait,omitempty"` | ||
| Timeout int `json:"timeout,omitempty"` | ||
| Name string `json:"name,omitempty"` | ||
| Addons []string `json:"addons,omitempty"` | ||
| Region string `json:"region,omitempty"` | ||
| CPUMemory string `json:"cpu_memory,omitempty"` | ||
| Replicas int `json:"replicas,omitempty"` | ||
| Free bool `json:"free,omitempty"` | ||
| Wait bool `json:"wait,omitempty"` | ||
| Timeout int `json:"timeout,omitempty"` | ||
| SetDefault bool `json:"set_default,omitempty"` | ||
| } | ||
|
|
||
| func (ServiceCreateInput) Schema() *jsonschema.Schema { | ||
|
|
@@ -129,6 +130,10 @@ func (ServiceCreateInput) Schema() *jsonschema.Schema { | |
| schema.Properties["timeout"].Default = util.Must(json.Marshal(30)) | ||
| schema.Properties["timeout"].Examples = []any{15, 30, 60} | ||
|
|
||
| schema.Properties["set_default"].Description = "Whether to set the newly created service as the default service. When true, the service will be set as the default for future commands." | ||
| schema.Properties["set_default"].Default = util.Must(json.Marshal(true)) | ||
| schema.Properties["set_default"].Examples = []any{true, false} | ||
|
|
||
| return schema | ||
| } | ||
|
|
||
|
|
@@ -256,25 +261,25 @@ Perfect for: | |
|
|
||
| // handleServiceList handles the tiger_service_list MCP tool | ||
| func (s *Server) handleServiceList(ctx context.Context, req *mcp.CallToolRequest, input ServiceListInput) (*mcp.CallToolResult, ServiceListOutput, error) { | ||
| // Create fresh API client with current credentials | ||
| apiClient, err := s.createAPIClient() | ||
| // Load config and validate project ID | ||
| cfg, err := s.loadConfigWithProjectID() | ||
| if err != nil { | ||
| return nil, ServiceListOutput{}, err | ||
| } | ||
|
|
||
| // Load fresh project ID from current config | ||
| projectID, err := s.loadProjectID() | ||
| // Create fresh API client with current credentials | ||
| apiClient, err := s.createAPIClient() | ||
| if err != nil { | ||
| return nil, ServiceListOutput{}, err | ||
| } | ||
|
|
||
| logging.Debug("MCP: Listing services", zap.String("project_id", projectID)) | ||
| logging.Debug("MCP: Listing services", zap.String("project_id", cfg.ProjectID)) | ||
|
|
||
| // Make API call to list services | ||
| ctx, cancel := context.WithTimeout(ctx, 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| resp, err := apiClient.GetProjectsProjectIdServicesWithResponse(ctx, projectID) | ||
| resp, err := apiClient.GetProjectsProjectIdServicesWithResponse(ctx, cfg.ProjectID) | ||
| if err != nil { | ||
| return nil, ServiceListOutput{}, fmt.Errorf("failed to list services: %w", err) | ||
| } | ||
|
|
@@ -308,27 +313,27 @@ func (s *Server) handleServiceList(ctx context.Context, req *mcp.CallToolRequest | |
|
|
||
| // handleServiceShow handles the tiger_service_show MCP tool | ||
| func (s *Server) handleServiceShow(ctx context.Context, req *mcp.CallToolRequest, input ServiceShowInput) (*mcp.CallToolResult, ServiceShowOutput, error) { | ||
| // Create fresh API client with current credentials | ||
| apiClient, err := s.createAPIClient() | ||
| // Load config and validate project ID | ||
| cfg, err := s.loadConfigWithProjectID() | ||
| if err != nil { | ||
| return nil, ServiceShowOutput{}, err | ||
| } | ||
|
|
||
| // Load fresh project ID from current config | ||
| projectID, err := s.loadProjectID() | ||
| // Create fresh API client with current credentials | ||
| apiClient, err := s.createAPIClient() | ||
| if err != nil { | ||
| return nil, ServiceShowOutput{}, err | ||
| } | ||
|
|
||
| logging.Debug("MCP: Showing service details", | ||
| zap.String("project_id", projectID), | ||
| zap.String("project_id", cfg.ProjectID), | ||
| zap.String("service_id", input.ServiceID)) | ||
|
|
||
| // Make API call to get service details | ||
| ctx, cancel := context.WithTimeout(ctx, 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| resp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, input.ServiceID) | ||
| resp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, cfg.ProjectID, input.ServiceID) | ||
| if err != nil { | ||
| return nil, ServiceShowOutput{}, fmt.Errorf("failed to get service details: %w", err) | ||
| } | ||
|
|
@@ -352,22 +357,22 @@ func (s *Server) handleServiceShow(ctx context.Context, req *mcp.CallToolRequest | |
| case 403: | ||
| return nil, ServiceShowOutput{}, fmt.Errorf("permission denied: insufficient access to service") | ||
| case 404: | ||
| return nil, ServiceShowOutput{}, fmt.Errorf("service '%s' not found in project '%s'", input.ServiceID, projectID) | ||
| return nil, ServiceShowOutput{}, fmt.Errorf("service '%s' not found in project '%s'", input.ServiceID, cfg.ProjectID) | ||
| default: | ||
| return nil, ServiceShowOutput{}, fmt.Errorf("API request failed with status %d", resp.StatusCode()) | ||
| } | ||
| } | ||
|
|
||
| // handleServiceCreate handles the tiger_service_create MCP tool | ||
| func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolRequest, input ServiceCreateInput) (*mcp.CallToolResult, ServiceCreateOutput, error) { | ||
| // Create fresh API client with current credentials | ||
| apiClient, err := s.createAPIClient() | ||
| // Load config and validate project ID | ||
| cfg, err := s.loadConfigWithProjectID() | ||
| if err != nil { | ||
| return nil, ServiceCreateOutput{}, err | ||
| } | ||
|
|
||
| // Load fresh project ID from current config | ||
| projectID, err := s.loadProjectID() | ||
| // Create fresh API client with current credentials | ||
| apiClient, err := s.createAPIClient() | ||
| if err != nil { | ||
| return nil, ServiceCreateOutput{}, err | ||
| } | ||
|
|
@@ -429,7 +434,7 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque | |
| } | ||
|
|
||
| logging.Debug("MCP: Creating service", | ||
| zap.String("project_id", projectID), | ||
| zap.String("project_id", cfg.ProjectID), | ||
| zap.String("name", input.Name), | ||
| zap.Strings("addons", input.Addons), | ||
| zap.String("region", input.Region), | ||
|
|
@@ -458,7 +463,7 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque | |
| ctx, cancel := context.WithTimeout(ctx, 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| resp, err := apiClient.PostProjectsProjectIdServicesWithResponse(ctx, projectID, serviceCreateReq) | ||
| resp, err := apiClient.PostProjectsProjectIdServicesWithResponse(ctx, cfg.ProjectID, serviceCreateReq) | ||
| if err != nil { | ||
| return nil, ServiceCreateOutput{}, fmt.Errorf("failed to create service: %w", err) | ||
| } | ||
|
|
@@ -497,11 +502,21 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque | |
| } | ||
| } | ||
|
|
||
| // Set as default service if requested (defaults to true) | ||
| if input.SetDefault { | ||
| if err := cfg.Set("service_id", serviceID); err != nil { | ||
| // Log warning but don't fail the service creation | ||
| logging.Debug("MCP: Failed to set service as default", zap.Error(err)) | ||
| } else { | ||
| logging.Debug("MCP: Set service as default", zap.String("service_id", serviceID)) | ||
| } | ||
| } | ||
|
Comment on lines
+505
to
+513
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the main change. |
||
|
|
||
| // If wait is explicitly requested, wait for service to be ready | ||
| if input.Wait { | ||
| timeout := time.Duration(input.Timeout) * time.Minute | ||
|
|
||
| output.Service, err = s.waitForServiceReady(apiClient, projectID, serviceID, timeout, serviceStatus) | ||
| output.Service, err = s.waitForServiceReady(apiClient, cfg.ProjectID, serviceID, timeout, serviceStatus) | ||
| if err != nil { | ||
| output.Message = fmt.Sprintf("Error: %s", err.Error()) | ||
| } else { | ||
|
|
@@ -523,20 +538,20 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque | |
|
|
||
| // handleServiceUpdatePassword handles the tiger_service_update_password MCP tool | ||
| func (s *Server) handleServiceUpdatePassword(ctx context.Context, req *mcp.CallToolRequest, input ServiceUpdatePasswordInput) (*mcp.CallToolResult, ServiceUpdatePasswordOutput, error) { | ||
| // Create fresh API client with current credentials | ||
| apiClient, err := s.createAPIClient() | ||
| // Load config and validate project ID | ||
| cfg, err := s.loadConfigWithProjectID() | ||
| if err != nil { | ||
| return nil, ServiceUpdatePasswordOutput{}, err | ||
| } | ||
|
|
||
| // Load fresh project ID from current config | ||
| projectID, err := s.loadProjectID() | ||
| // Create fresh API client with current credentials | ||
| apiClient, err := s.createAPIClient() | ||
| if err != nil { | ||
| return nil, ServiceUpdatePasswordOutput{}, err | ||
| } | ||
|
|
||
| logging.Debug("MCP: Updating service password", | ||
| zap.String("project_id", projectID), | ||
| zap.String("project_id", cfg.ProjectID), | ||
| zap.String("service_id", input.ServiceID)) | ||
|
|
||
| // Prepare password update request | ||
|
|
@@ -548,7 +563,7 @@ func (s *Server) handleServiceUpdatePassword(ctx context.Context, req *mcp.CallT | |
| ctx, cancel := context.WithTimeout(ctx, 30*time.Second) | ||
| defer cancel() | ||
|
|
||
| resp, err := apiClient.PostProjectsProjectIdServicesServiceIdUpdatePasswordWithResponse(ctx, projectID, input.ServiceID, updateReq) | ||
| resp, err := apiClient.PostProjectsProjectIdServicesServiceIdUpdatePasswordWithResponse(ctx, cfg.ProjectID, input.ServiceID, updateReq) | ||
| if err != nil { | ||
| return nil, ServiceUpdatePasswordOutput{}, fmt.Errorf("failed to update service password: %w", err) | ||
| } | ||
|
|
@@ -561,7 +576,7 @@ func (s *Server) handleServiceUpdatePassword(ctx context.Context, req *mcp.CallT | |
| } | ||
|
|
||
| // Get service details for password storage (similar to CLI implementation) | ||
| serviceResp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, input.ServiceID) | ||
| serviceResp, err := apiClient.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, cfg.ProjectID, input.ServiceID) | ||
| if err == nil && serviceResp.StatusCode() == 200 && serviceResp.JSON200 != nil { | ||
| // Save the new password using the shared util function | ||
| result, err := password.SavePasswordWithResult(api.Service(*serviceResp.JSON200), input.Password) | ||
|
|
@@ -580,7 +595,7 @@ func (s *Server) handleServiceUpdatePassword(ctx context.Context, req *mcp.CallT | |
| case 403: | ||
| return nil, ServiceUpdatePasswordOutput{}, fmt.Errorf("permission denied: insufficient access to update service password") | ||
| case 404: | ||
| return nil, ServiceUpdatePasswordOutput{}, fmt.Errorf("service '%s' not found in project '%s'", input.ServiceID, projectID) | ||
| return nil, ServiceUpdatePasswordOutput{}, fmt.Errorf("service '%s' not found in project '%s'", input.ServiceID, cfg.ProjectID) | ||
| case 400: | ||
| var errorMsg string | ||
| if resp.JSON400 != nil && resp.JSON400.Message != nil { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idk why we were re-loading the config here. I don't think it's necessary to reload it, so I updated the code to just pass in the config that was already loaded.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It doesn't "re-load" so much as grab a copy. Agreed that this is weird/unnecessary, and passing in is better. There are still things like this I'd like to improve about the config, but I was avoiding broader changes in #36