From 9d68dc5cc1fd261cb365ef4f2abb7295374b8ad6 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Wed, 8 Oct 2025 11:16:43 -0400 Subject: [PATCH 1/8] Add password and connection string to service_create MCP tool response --- internal/tiger/mcp/service_tools.go | 60 ++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/internal/tiger/mcp/service_tools.go b/internal/tiger/mcp/service_tools.go index 63e6eae8..5c37b325 100644 --- a/internal/tiger/mcp/service_tools.go +++ b/internal/tiger/mcp/service_tools.go @@ -101,17 +101,19 @@ func (ServiceShowOutput) Schema() *jsonschema.Schema { // ServiceDetail represents detailed service information type ServiceDetail struct { - ServiceID string `json:"id" jsonschema:"Service identifier (10-character alphanumeric string)"` - Name string `json:"name"` - Status string `json:"status" jsonschema:"Service status (e.g., READY, PAUSED, CONFIGURING, UPGRADING)"` - Type string `json:"type"` - Region string `json:"region"` - Created string `json:"created,omitempty"` - Resources *ResourceInfo `json:"resources,omitempty"` - Replicas int `json:"replicas,omitempty" jsonschema:"Number of HA replicas (0=single node/no HA, 1+=HA enabled)"` - DirectEndpoint string `json:"direct_endpoint,omitempty" jsonschema:"Direct database connection endpoint"` - PoolerEndpoint string `json:"pooler_endpoint,omitempty" jsonschema:"Connection pooler endpoint"` - Paused bool `json:"paused"` + ServiceID string `json:"id" jsonschema:"Service identifier (10-character alphanumeric string)"` + Name string `json:"name"` + Status string `json:"status" jsonschema:"Service status (e.g., READY, PAUSED, CONFIGURING, UPGRADING)"` + Type string `json:"type"` + Region string `json:"region"` + Created string `json:"created,omitempty"` + Resources *ResourceInfo `json:"resources,omitempty"` + Replicas int `json:"replicas,omitempty" jsonschema:"Number of HA replicas (0=single node/no HA, 1+=HA enabled)"` + DirectEndpoint string `json:"direct_endpoint,omitempty" jsonschema:"Direct database connection endpoint"` + PoolerEndpoint string `json:"pooler_endpoint,omitempty" jsonschema:"Connection pooler endpoint"` + Paused bool `json:"paused"` + Password string `json:"password,omitempty" jsonschema:"Password for tsdbadmin user (only included if with_password=true)"` + ConnectionString string `json:"connection_string" jsonschema:"PostgreSQL connection string (password embedded only if with_password=true)"` } func (ServiceDetail) Schema() *jsonschema.Schema { @@ -131,6 +133,7 @@ type ServiceCreateInput struct { Wait bool `json:"wait,omitempty"` TimeoutMinutes *int `json:"timeout_minutes,omitempty"` SetDefault bool `json:"set_default,omitempty"` + WithPassword bool `json:"with_password,omitempty"` } func (ServiceCreateInput) Schema() *jsonschema.Schema { @@ -174,6 +177,10 @@ func (ServiceCreateInput) Schema() *jsonschema.Schema { schema.Properties["set_default"].Default = util.Must(json.Marshal(true)) schema.Properties["set_default"].Examples = []any{true, false} + schema.Properties["with_password"].Description = "Whether to include the initial password and password in connection string in the response. When false (default), password is excluded from response but still saved to configured password storage." + schema.Properties["with_password"].Default = util.Must(json.Marshal(false)) + schema.Properties["with_password"].Examples = []any{false, true} + return schema } @@ -501,12 +508,6 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque serviceID := util.Deref(service.ServiceId) serviceStatus := util.DerefStr(service.Status) - // Capture initial password from creation response and save it immediately - var initialPassword string - if service.InitialPassword != nil { - initialPassword = *service.InitialPassword - } - output := ServiceCreateOutput{ Service: s.convertToServiceDetail(service), Message: "Service creation request accepted. The service may still be provisioning.", @@ -514,8 +515,8 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque // Save password immediately after service creation, before any waiting // This ensures the password is stored even if the wait fails or is interrupted - if initialPassword != "" { - result, err := password.SavePasswordWithResult(api.Service(service), initialPassword) + if service.InitialPassword != nil { + result, err := password.SavePasswordWithResult(api.Service(service), *service.InitialPassword) output.PasswordStorage = &result if err != nil { logging.Debug("MCP: Password storage failed", zap.Error(err)) @@ -524,6 +525,27 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque } } + // Include password in ServiceDetail if requested + if input.WithPassword && service.InitialPassword != nil { + output.Service.Password = *service.InitialPassword + } + + // Always include connection string in ServiceDetail + // Password is embedded in connection string only if with_password=true + passwordMode := password.PasswordExclude + if input.WithPassword { + passwordMode = password.PasswordRequired + } + if connectionString, err := password.BuildConnectionString(api.Service(service), password.ConnectionStringOptions{ + Pooled: false, + Role: "tsdbadmin", + PasswordMode: passwordMode, + }); err != nil { + logging.Debug("MCP: Failed to build connection string", zap.Error(err)) + } else { + output.Service.ConnectionString = connectionString + } + // Set as default service if requested (defaults to true) if input.SetDefault { if err := cfg.Set("service_id", serviceID); err != nil { From d29b2da8feba6d9a699dc8ba24da81ca2ed6ec4d Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Wed, 8 Oct 2025 11:43:03 -0400 Subject: [PATCH 2/8] Fix bug related to not showing initial password when with_password=true but password_storage=none --- internal/tiger/mcp/service_tools.go | 15 ++-- internal/tiger/password/connection.go | 103 ++++++++++++++++---------- 2 files changed, 70 insertions(+), 48 deletions(-) diff --git a/internal/tiger/mcp/service_tools.go b/internal/tiger/mcp/service_tools.go index 5c37b325..bd02e3d4 100644 --- a/internal/tiger/mcp/service_tools.go +++ b/internal/tiger/mcp/service_tools.go @@ -526,20 +526,17 @@ func (s *Server) handleServiceCreate(ctx context.Context, req *mcp.CallToolReque } // Include password in ServiceDetail if requested - if input.WithPassword && service.InitialPassword != nil { - output.Service.Password = *service.InitialPassword + if input.WithPassword { + output.Service.Password = util.Deref(service.InitialPassword) } // Always include connection string in ServiceDetail // Password is embedded in connection string only if with_password=true - passwordMode := password.PasswordExclude - if input.WithPassword { - passwordMode = password.PasswordRequired - } if connectionString, err := password.BuildConnectionString(api.Service(service), password.ConnectionStringOptions{ - Pooled: false, - Role: "tsdbadmin", - PasswordMode: passwordMode, + Pooled: false, + Role: "tsdbadmin", + PasswordMode: password.GetPasswordMode(input.WithPassword), + InitialPassword: util.Deref(service.InitialPassword), }); err != nil { logging.Debug("MCP: Failed to build connection string", zap.Error(err)) } else { diff --git a/internal/tiger/password/connection.go b/internal/tiger/password/connection.go index 38aaf4e8..f5c708b7 100644 --- a/internal/tiger/password/connection.go +++ b/internal/tiger/password/connection.go @@ -24,6 +24,16 @@ const ( PasswordOptional ) +// GetPasswordMode is a helper function for getting a [PasswordMode] from a +// boolean. It only ever returns [PasswordRequired]/[PasswordExcluded]. If you +// need [PasswordOptional], do not use this function. +func GetPasswordMode(required bool) PasswordMode { + if required { + return PasswordRequired + } + return PasswordExclude +} + // ConnectionStringOptions configures how the connection string is built type ConnectionStringOptions struct { // Pooled determines whether to use the pooler endpoint (if available) @@ -35,6 +45,11 @@ type ConnectionStringOptions struct { // PasswordMode determines how passwords are handled PasswordMode PasswordMode + // InitialPassword is an optional password to use directly (e.g., from service creation response) + // If provided and PasswordMode is PasswordRequired or PasswordOptional, this password will be used + // instead of fetching from password storage. This is useful when password_storage=none. + InitialPassword string + // WarnWriter is an optional writer for warning messages (e.g., when pooler is requested but not available) // If nil, warnings are suppressed WarnWriter io.Writer @@ -74,11 +89,8 @@ func BuildConnectionString(service api.Service, opts ConnectionStringOptions) (s return "", fmt.Errorf("service endpoint not available") } - var endpoint *api.Endpoint - var host string - var port int - // Use pooler endpoint if requested and available, otherwise use direct endpoint + var endpoint *api.Endpoint if opts.Pooled && service.ConnectionPooler != nil && service.ConnectionPooler.Endpoint != nil { endpoint = service.ConnectionPooler.Endpoint } else { @@ -92,65 +104,78 @@ func BuildConnectionString(service api.Service, opts ConnectionStringOptions) (s if endpoint.Host == nil { return "", fmt.Errorf("endpoint host not available") } - host = *endpoint.Host + host := *endpoint.Host + port := 5432 // Default PostgreSQL port if endpoint.Port != nil { port = *endpoint.Port - } else { - port = 5432 // Default PostgreSQL port } // Database is always "tsdb" for TimescaleDB/PostgreSQL services database := "tsdb" // Build connection string in PostgreSQL URI format - var connectionString string - switch opts.PasswordMode { case PasswordRequired: - // Password is required - error if unavailable - storage := GetPasswordStorage() - password, err := storage.Get(service) - if err != nil { - // Provide specific error messages based on storage type - switch storage.(type) { - case *NoStorage: - return "", fmt.Errorf("password storage is disabled (--password-storage=none)") - case *KeyringStorage: - return "", fmt.Errorf("no password found in keyring for this service") - case *PgpassStorage: - return "", fmt.Errorf("no password found in ~/.pgpass for this service") - default: - return "", fmt.Errorf("failed to retrieve password: %w", err) + // Password is required - use InitialPassword if provided, otherwise fetch from storage + var password string + if opts.InitialPassword != "" { + password = opts.InitialPassword + } else { + var err error + if password, err = GetPassword(service); err != nil { + return "", err } } - if password == "" { - return "", fmt.Errorf("no password available for service") - } - // Include password in connection string - connectionString = fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=require", opts.Role, password, host, port, database) - + return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=require", opts.Role, password, host, port, database), nil case PasswordOptional: - // Try to include password, but don't error if unavailable - storage := GetPasswordStorage() - password, err := storage.Get(service) + // Try to include password - use InitialPassword if provided, otherwise try fetching from storage + var password string + if opts.InitialPassword != "" { + password = opts.InitialPassword + } else { + password, _ = GetPassword(service) // Ignore error for optional mode + } - // Only include password if we successfully retrieved it - if err == nil && password != "" { - connectionString = fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=require", opts.Role, password, host, port, database) + // Only include password if we have one + if password != "" { + return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=require", opts.Role, password, host, port, database), nil } else { // Fall back to connection string without password - connectionString = fmt.Sprintf("postgresql://%s@%s:%d/%s?sslmode=require", opts.Role, host, port, database) + return fmt.Sprintf("postgresql://%s@%s:%d/%s?sslmode=require", opts.Role, host, port, database), nil } - default: // PasswordExclude // Build connection string without password (default behavior) // Password is handled separately via PGPASSWORD env var or ~/.pgpass file // This ensures credentials are never visible in process arguments - connectionString = fmt.Sprintf("postgresql://%s@%s:%d/%s?sslmode=require", opts.Role, host, port, database) + return fmt.Sprintf("postgresql://%s@%s:%d/%s?sslmode=require", opts.Role, host, port, database), nil } +} - return connectionString, nil +// GetPassword fetches the password for the specified service from the +// configured password storage mechanism. It returns an error if it fails to +// find the password. +func GetPassword(service api.Service) (string, error) { + storage := GetPasswordStorage() + password, err := storage.Get(service) + if err != nil { + // Provide specific error messages based on storage type + switch storage.(type) { + case *NoStorage: + return "", fmt.Errorf("password storage is disabled (--password-storage=none)") + case *KeyringStorage: + return "", fmt.Errorf("no password found in keyring for this service") + case *PgpassStorage: + return "", fmt.Errorf("no password found in ~/.pgpass for this service") + default: + return "", fmt.Errorf("failed to retrieve password: %w", err) + } + } + + if password == "" { + return "", fmt.Errorf("no password available for service") + } + return password, nil } From cf0c48229465402da54c557a3bce518c605a1eec Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Wed, 8 Oct 2025 12:30:32 -0400 Subject: [PATCH 3/8] Update service_show to work the same as service_create with respect to connection string and password --- internal/tiger/mcp/service_tools.go | 41 +++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/internal/tiger/mcp/service_tools.go b/internal/tiger/mcp/service_tools.go index bd02e3d4..ecc22abf 100644 --- a/internal/tiger/mcp/service_tools.go +++ b/internal/tiger/mcp/service_tools.go @@ -79,14 +79,24 @@ func setServiceIDSchemaProperties(schema *jsonschema.Schema) { schema.Properties["service_id"].Pattern = "^[a-z0-9]{10}$" } +// setServiceIDSchemaProperties sets common with_password schema properties +func setWithPasswordSchemaProperties(schema *jsonschema.Schema) { + schema.Properties["with_password"].Description = "Whether to include the password in the response and in the returned connection string." + schema.Properties["with_password"].Default = util.Must(json.Marshal(false)) + schema.Properties["with_password"].Examples = []any{false, true} +} + // ServiceShowInput represents input for service_show type ServiceShowInput struct { - ServiceID string `json:"service_id"` + ServiceID string `json:"service_id"` + WithPassword bool `json:"with_password,omitempty"` } func (ServiceShowInput) Schema() *jsonschema.Schema { schema := util.Must(jsonschema.For[ServiceShowInput](nil)) setServiceIDSchemaProperties(schema) + setWithPasswordSchemaProperties(schema) + return schema } @@ -108,7 +118,7 @@ type ServiceDetail struct { Region string `json:"region"` Created string `json:"created,omitempty"` Resources *ResourceInfo `json:"resources,omitempty"` - Replicas int `json:"replicas,omitempty" jsonschema:"Number of HA replicas (0=single node/no HA, 1+=HA enabled)"` + Replicas int `json:"replicas" jsonschema:"Number of HA replicas (0=single node/no HA, 1+=HA enabled)"` DirectEndpoint string `json:"direct_endpoint,omitempty" jsonschema:"Direct database connection endpoint"` PoolerEndpoint string `json:"pooler_endpoint,omitempty" jsonschema:"Connection pooler endpoint"` Paused bool `json:"paused"` @@ -177,9 +187,7 @@ func (ServiceCreateInput) Schema() *jsonschema.Schema { schema.Properties["set_default"].Default = util.Must(json.Marshal(true)) schema.Properties["set_default"].Examples = []any{true, false} - schema.Properties["with_password"].Description = "Whether to include the initial password and password in connection string in the response. When false (default), password is excluded from response but still saved to configured password storage." - schema.Properties["with_password"].Default = util.Must(json.Marshal(false)) - schema.Properties["with_password"].Examples = []any{false, true} + setWithPasswordSchemaProperties(schema) return schema } @@ -379,6 +387,29 @@ func (s *Server) handleServiceShow(ctx context.Context, req *mcp.CallToolRequest Service: s.convertToServiceDetail(service), } + // Include password in ServiceDetail if requested + // Note: service_show doesn't have access to InitialPassword, so we fetch from storage + if input.WithPassword { + if passwd, err := password.GetPassword(service); err != nil { + logging.Debug("MCP: Failed to retrieve password from storage", zap.Error(err)) + } else { + output.Service.Password = passwd + } + } + + // Always include connection string in ServiceDetail + // Password is embedded in connection string only if with_password=true + // Note: InitialPassword is empty string here since service_show doesn't have it + if connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{ + Pooled: false, + Role: "tsdbadmin", + PasswordMode: password.GetPasswordMode(input.WithPassword), + }); err != nil { + logging.Debug("MCP: Failed to build connection string", zap.Error(err)) + } else { + output.Service.ConnectionString = connectionString + } + return nil, output, nil case 401: From 1229ea716e473deda16f6a533d8f055e89d83290 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Wed, 8 Oct 2025 12:34:02 -0400 Subject: [PATCH 4/8] Update spec_mcp.md --- internal/tiger/mcp/service_tools.go | 2 +- specs/spec_mcp.md | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/tiger/mcp/service_tools.go b/internal/tiger/mcp/service_tools.go index ecc22abf..4a5cdf2e 100644 --- a/internal/tiger/mcp/service_tools.go +++ b/internal/tiger/mcp/service_tools.go @@ -81,7 +81,7 @@ func setServiceIDSchemaProperties(schema *jsonschema.Schema) { // setServiceIDSchemaProperties sets common with_password schema properties func setWithPasswordSchemaProperties(schema *jsonschema.Schema) { - schema.Properties["with_password"].Description = "Whether to include the password in the response and in the returned connection string." + schema.Properties["with_password"].Description = "Whether to include the password in the response and connection string." schema.Properties["with_password"].Default = util.Must(json.Marshal(false)) schema.Properties["with_password"].Examples = []any{false, true} } diff --git a/specs/spec_mcp.md b/specs/spec_mcp.md index 993bf2ff..a78b8f52 100644 --- a/specs/spec_mcp.md +++ b/specs/spec_mcp.md @@ -160,8 +160,9 @@ Show details of a specific service. **Parameters:** - `service_id` (string, required): Service ID to show +- `with_password` (boolean, optional): Include password in response and connection string - default: false -**Returns:** Detailed service object with configuration, endpoints, and status. +**Returns:** Detailed service object with configuration, endpoints, status, and connection string. When `with_password=true`, the response includes the password field and the password is embedded in the connection string. When `with_password=false` (default), the connection string is still included but without the password embedded. #### `tiger_service_create` Create a new database service. @@ -175,10 +176,11 @@ Create a new database service. - `wait` (boolean, optional): Wait for service to be ready - default: false - `timeout` (number, optional): Timeout for waiting in minutes - default: 30 - `set_default` (boolean, optional): Set the newly created service as the default service for future commands - default: true +- `with_password` (boolean, optional): Include password in response and connection string - default: false -**Returns:** Service object with creation status and details. +**Returns:** Service object with creation status, details, and connection string. When `with_password=true`, the response includes the initial password field and the password is embedded in the connection string. When `with_password=false` (default), the connection string is still included but without the password embedded. -**Note:** This tool automatically stores the database password using the same method as the CLI (keyring, pgpass file, etc.). +**Note:** This tool automatically stores the database password using the same method as the CLI (keyring, pgpass file, etc.), regardless of the `with_password` parameter value. #### `tiger_service_delete` Delete a database service. From 86d64ab4db54a74d79c4d3ac4bf7fdd5cc828b6f Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Wed, 8 Oct 2025 12:40:28 -0400 Subject: [PATCH 5/8] De-duplicate code --- internal/tiger/cmd/db.go | 24 ------------------------ internal/tiger/cmd/service.go | 2 +- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/internal/tiger/cmd/db.go b/internal/tiger/cmd/db.go index 286234c9..f7d158d1 100644 --- a/internal/tiger/cmd/db.go +++ b/internal/tiger/cmd/db.go @@ -248,30 +248,6 @@ func buildDbCmd() *cobra.Command { return cmd } -func getServicePassword(service api.Service) (string, error) { - // Get password from storage if requested - storage := password.GetPasswordStorage() - passwd, err := storage.Get(service) - if err != nil { - // Provide specific error messages based on storage type - switch storage.(type) { - case *password.NoStorage: - return "", fmt.Errorf("password storage is disabled (--password-storage=none)") - case *password.KeyringStorage: - return "", fmt.Errorf("no password found in keyring for this service") - case *password.PgpassStorage: - return "", fmt.Errorf("no password found in ~/.pgpass for this service") - default: - return "", fmt.Errorf("failed to retrieve password: %w", err) - } - } - - if passwd == "" { - return "", fmt.Errorf("no password available for service") - } - return passwd, nil -} - // getServiceDetails is a helper that handles common service lookup logic and returns the service details func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) { // Get config diff --git a/internal/tiger/cmd/service.go b/internal/tiger/cmd/service.go index 390b05bb..2f729b1f 100644 --- a/internal/tiger/cmd/service.go +++ b/internal/tiger/cmd/service.go @@ -801,7 +801,7 @@ func prepareServiceForOutput(service api.Service, withPassword bool, output io.W if !withPassword { outputSvc.InitialPassword = nil } else if service.InitialPassword == nil { - password, err := getServicePassword(service) + password, err := password.GetPassword(service) if err != nil { fmt.Fprintf(output, "⚠️ Warning: Failed to retrieve stored password: %v\n", err) } From d2cd0664ab9df9ff6f1b8d0f2b22b6f4e72b4e9c Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Wed, 8 Oct 2025 12:46:16 -0400 Subject: [PATCH 6/8] Clean up some code --- internal/tiger/cmd/db.go | 7 +------ internal/tiger/cmd/service.go | 26 +++++++++++--------------- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/internal/tiger/cmd/db.go b/internal/tiger/cmd/db.go index f7d158d1..0446d522 100644 --- a/internal/tiger/cmd/db.go +++ b/internal/tiger/cmd/db.go @@ -60,15 +60,10 @@ Examples: return err } - passwordMode := password.PasswordExclude - if dbConnectionStringWithPassword { - passwordMode = password.PasswordRequired - } - connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{ Pooled: dbConnectionStringPooled, Role: dbConnectionStringRole, - PasswordMode: passwordMode, + PasswordMode: password.GetPasswordMode(dbConnectionStringWithPassword), WarnWriter: cmd.ErrOrStderr(), }) if err != nil { diff --git a/internal/tiger/cmd/service.go b/internal/tiger/cmd/service.go index 2f729b1f..72ed7311 100644 --- a/internal/tiger/cmd/service.go +++ b/internal/tiger/cmd/service.go @@ -797,6 +797,17 @@ func prepareServiceForOutput(service api.Service, withPassword bool, output io.W Service: service, } + // Build connection string + if connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{ + Pooled: false, + Role: "tsdbadmin", + PasswordMode: password.GetPasswordMode(withPassword), + InitialPassword: util.Deref(service.InitialPassword), + WarnWriter: output, + }); err == nil { + outputSvc.ConnectionString = &connectionString + } + // Remove password if not requested if !withPassword { outputSvc.InitialPassword = nil @@ -808,21 +819,6 @@ func prepareServiceForOutput(service api.Service, withPassword bool, output io.W outputSvc.Password = &password } - // Build connection string - passwordMode := password.PasswordExclude - if withPassword { - passwordMode = password.PasswordRequired - } - connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{ - Pooled: false, - Role: "tsdbadmin", - PasswordMode: passwordMode, - WarnWriter: output, - }) - if err == nil { - outputSvc.ConnectionString = &connectionString - } - return outputSvc } From 1a87096612442995ee82a6affb69a6d861aac525 Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Wed, 8 Oct 2025 13:06:42 -0400 Subject: [PATCH 7/8] Remove unnecessary comment --- internal/tiger/mcp/service_tools.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/tiger/mcp/service_tools.go b/internal/tiger/mcp/service_tools.go index 4a5cdf2e..b7762d09 100644 --- a/internal/tiger/mcp/service_tools.go +++ b/internal/tiger/mcp/service_tools.go @@ -399,7 +399,6 @@ func (s *Server) handleServiceShow(ctx context.Context, req *mcp.CallToolRequest // Always include connection string in ServiceDetail // Password is embedded in connection string only if with_password=true - // Note: InitialPassword is empty string here since service_show doesn't have it if connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{ Pooled: false, Role: "tsdbadmin", From ac998a6fa979fe816c0b39fc41a19ffefe50e75d Mon Sep 17 00:00:00 2001 From: Nathan Cochran Date: Thu, 9 Oct 2025 17:00:12 -0400 Subject: [PATCH 8/8] Minor cleanup --- internal/tiger/cmd/service.go | 16 ++------------ internal/tiger/mcp/service_tools.go | 11 +--------- internal/tiger/password/connection.go | 31 +++++---------------------- 3 files changed, 8 insertions(+), 50 deletions(-) diff --git a/internal/tiger/cmd/service.go b/internal/tiger/cmd/service.go index b3b42d21..1a45f76c 100644 --- a/internal/tiger/cmd/service.go +++ b/internal/tiger/cmd/service.go @@ -423,15 +423,9 @@ Note: You can specify both CPU and memory together, or specify only one (the oth fmt.Fprintf(statusOutput, "✅ Service creation request accepted!\n") fmt.Fprintf(statusOutput, "📋 Service ID: %s\n", serviceID) - // Capture initial password from creation response and save it immediately - var initialPassword string - if service.InitialPassword != nil { - initialPassword = *service.InitialPassword - } - // Save password immediately after service creation, before any waiting // This ensures users have access even if they interrupt the wait or it fails - passwordSaved := handlePasswordSaving(service, initialPassword, statusOutput) + passwordSaved := handlePasswordSaving(service, util.Deref(service.InitialPassword), statusOutput) // Set as default service unless --no-set-default is specified if !createNoSetDefault { @@ -1300,14 +1294,8 @@ Examples: fmt.Fprintf(statusOutput, "✅ Fork request accepted!\n") fmt.Fprintf(statusOutput, "📋 New Service ID: %s\n", forkedServiceID) - // Capture initial password from fork response and save it immediately - var initialPassword string - if forkedService.InitialPassword != nil { - initialPassword = *forkedService.InitialPassword - } - // Save password immediately after service fork - passwordSaved := handlePasswordSaving(forkedService, initialPassword, statusOutput) + passwordSaved := handlePasswordSaving(forkedService, util.Deref(forkedService.InitialPassword), statusOutput) // Set as default service unless --no-set-default is used if !forkNoSetDefault { diff --git a/internal/tiger/mcp/service_tools.go b/internal/tiger/mcp/service_tools.go index 187e664d..68e870f3 100644 --- a/internal/tiger/mcp/service_tools.go +++ b/internal/tiger/mcp/service_tools.go @@ -379,16 +379,6 @@ func (s *Server) handleServiceGet(ctx context.Context, req *mcp.CallToolRequest, Service: s.convertToServiceDetail(service), } - // Include password in ServiceDetail if requested - // Note: service_show doesn't have access to InitialPassword, so we fetch from storage - if input.WithPassword { - if passwd, err := password.GetPassword(service); err != nil { - logging.Debug("MCP: Failed to retrieve password from storage", zap.Error(err)) - } else { - output.Service.Password = passwd - } - } - // Always include connection string in ServiceDetail // Password is embedded in connection string only if with_password=true if details, err := password.GetConnectionDetails(service, password.ConnectionDetailsOptions{ @@ -398,6 +388,7 @@ func (s *Server) handleServiceGet(ctx context.Context, req *mcp.CallToolRequest, }); err != nil { logging.Debug("MCP: Failed to build connection string", zap.Error(err)) } else { + output.Service.Password = details.Password output.Service.ConnectionString = details.String() } diff --git a/internal/tiger/password/connection.go b/internal/tiger/password/connection.go index e862d4b7..071c68b5 100644 --- a/internal/tiger/password/connection.go +++ b/internal/tiger/password/connection.go @@ -95,34 +95,13 @@ func GetConnectionDetails(service api.Service, opts ConnectionDetailsOptions) (* } if opts.PasswordMode == PasswordRequired || opts.PasswordMode == PasswordOptional { - var password string if opts.InitialPassword != "" { - password = opts.InitialPassword + details.Password = opts.InitialPassword + } else if password, err := GetPassword(service); err != nil && opts.PasswordMode == PasswordRequired { + return nil, err + } else { + details.Password = password } - if password == "" { - storage := GetPasswordStorage() - if storedPassword, err := storage.Get(service); err != nil && opts.PasswordMode == PasswordRequired { - // Provide specific error messages based on storage type - switch storage.(type) { - case *NoStorage: - return nil, fmt.Errorf("password storage is disabled (--password-storage=none)") - case *KeyringStorage: - return nil, fmt.Errorf("no password found in keyring for this service") - case *PgpassStorage: - return nil, fmt.Errorf("no password found in ~/.pgpass for this service") - default: - return nil, fmt.Errorf("failed to retrieve password: %w", err) - } - } else { - password = storedPassword - } - } - - if password == "" && opts.PasswordMode == PasswordRequired { - return nil, fmt.Errorf("no password available for service") - } - - details.Password = password } return details, nil