Skip to content

Commit daa909a

Browse files
Add MCP tool for querying a service (#42)
1 parent 78cf4e9 commit daa909a

File tree

8 files changed

+925
-411
lines changed

8 files changed

+925
-411
lines changed

internal/tiger/cmd/db.go

Lines changed: 30 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"io"
87
"os"
98
"os/exec"
109
"time"
@@ -61,7 +60,17 @@ Examples:
6160
return err
6261
}
6362

64-
connectionString, err := buildConnectionString(service, dbConnectionStringPooled, dbConnectionStringRole, dbConnectionStringWithPassword, cmd.ErrOrStderr())
63+
passwordMode := password.PasswordExclude
64+
if dbConnectionStringWithPassword {
65+
passwordMode = password.PasswordRequired
66+
}
67+
68+
connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
69+
Pooled: dbConnectionStringPooled,
70+
Role: dbConnectionStringRole,
71+
PasswordMode: passwordMode,
72+
WarnWriter: cmd.ErrOrStderr(),
73+
})
6574
if err != nil {
6675
return fmt.Errorf("failed to build connection string: %w", err)
6776
}
@@ -133,8 +142,12 @@ Examples:
133142
return fmt.Errorf("psql client not found. Please install PostgreSQL client tools")
134143
}
135144

136-
// Get connection string using existing logic
137-
connectionString, err := buildConnectionString(service, dbConnectPooled, dbConnectRole, false, cmd.ErrOrStderr())
145+
connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
146+
Pooled: dbConnectPooled,
147+
Role: dbConnectRole,
148+
PasswordMode: password.PasswordExclude,
149+
WarnWriter: cmd.ErrOrStderr(),
150+
})
138151
if err != nil {
139152
return fmt.Errorf("failed to build connection string: %w", err)
140153
}
@@ -192,8 +205,13 @@ Examples:
192205
return exitWithCode(ExitInvalidParameters, err)
193206
}
194207

195-
// Build connection string for testing
196-
connectionString, err := buildConnectionString(service, dbTestConnectionPooled, dbTestConnectionRole, false, cmd.ErrOrStderr())
208+
// Build connection string for testing with password (if available)
209+
connectionString, err := password.BuildConnectionString(service, password.ConnectionStringOptions{
210+
Pooled: dbTestConnectionPooled,
211+
Role: dbTestConnectionRole,
212+
PasswordMode: password.PasswordOptional,
213+
WarnWriter: cmd.ErrOrStderr(),
214+
})
197215
if err != nil {
198216
return exitWithCode(ExitInvalidParameters, fmt.Errorf("failed to build connection string: %w", err))
199217
}
@@ -204,7 +222,7 @@ Examples:
204222
}
205223

206224
// Test the connection
207-
return testDatabaseConnection(connectionString, dbTestConnectionTimeout, service, cmd)
225+
return testDatabaseConnection(cmd.Context(), connectionString, dbTestConnectionTimeout, cmd)
208226
},
209227
}
210228

@@ -254,62 +272,6 @@ func getServicePassword(service api.Service) (string, error) {
254272
return passwd, nil
255273
}
256274

257-
// buildConnectionString creates a PostgreSQL connection string from service details
258-
func buildConnectionString(service api.Service, pooled bool, role string, withPassword bool, output io.Writer) (string, error) {
259-
if service.Endpoint == nil {
260-
return "", fmt.Errorf("service endpoint not available")
261-
}
262-
263-
var endpoint *api.Endpoint
264-
var host string
265-
var port int
266-
267-
// Use pooler endpoint if requested and available, otherwise use direct endpoint
268-
if pooled && service.ConnectionPooler != nil && service.ConnectionPooler.Endpoint != nil {
269-
endpoint = service.ConnectionPooler.Endpoint
270-
} else {
271-
// If pooled was requested but no pooler is available, warn the user
272-
if pooled {
273-
fmt.Fprintf(output, "⚠️ Warning: Connection pooler not available for this service, using direct connection\n")
274-
}
275-
endpoint = service.Endpoint
276-
}
277-
278-
if endpoint.Host == nil {
279-
return "", fmt.Errorf("endpoint host not available")
280-
}
281-
host = *endpoint.Host
282-
283-
if endpoint.Port != nil {
284-
port = *endpoint.Port
285-
} else {
286-
port = 5432 // Default PostgreSQL port
287-
}
288-
289-
// Database is always "tsdb" for TimescaleDB/PostgreSQL services
290-
database := "tsdb"
291-
292-
// Build connection string in PostgreSQL URI format
293-
var connectionString string
294-
if withPassword {
295-
// Get password from storage if requested
296-
passwd, err := getServicePassword(service)
297-
if err != nil {
298-
return "", err
299-
}
300-
301-
// Include password in connection string
302-
connectionString = fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?sslmode=require", role, passwd, host, port, database)
303-
} else {
304-
// Build connection string without password (default behavior)
305-
// Password is handled separately via PGPASSWORD env var or ~/.pgpass file
306-
// This ensures credentials are never visible in process arguments
307-
connectionString = fmt.Sprintf("postgresql://%s@%s:%d/%s?sslmode=require", role, host, port, database)
308-
}
309-
310-
return connectionString, nil
311-
}
312-
313275
// getServiceDetails is a helper that handles common service lookup logic and returns the service details
314276
func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) {
315277
// Get config
@@ -350,7 +312,7 @@ func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) {
350312
}
351313

352314
// Fetch service details
353-
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
315+
ctx, cancel := context.WithTimeout(cmd.Context(), 30*time.Second)
354316
defer cancel()
355317

356318
resp, err := client.GetProjectsProjectIdServicesServiceIdWithResponse(ctx, projectID, serviceID)
@@ -437,50 +399,18 @@ func buildPsqlCommand(connectionString, psqlPath string, additionalFlags []strin
437399
return psqlCmd
438400
}
439401

440-
// buildConnectionConfig creates a pgx connection config with proper password handling
441-
func buildConnectionConfig(connectionString string, service api.Service) (*pgx.ConnConfig, error) {
442-
// Parse the connection string first to validate it
443-
config, err := pgx.ParseConfig(connectionString)
444-
if err != nil {
445-
return nil, err
446-
}
447-
448-
// Set password from keyring storage if available
449-
// pgpass storage works automatically since pgx checks ~/.pgpass file
450-
storage := password.GetPasswordStorage()
451-
if _, isKeyring := storage.(*password.KeyringStorage); isKeyring {
452-
if password, err := storage.Get(service); err == nil && password != "" {
453-
config.Password = password
454-
}
455-
// Note: If keyring password retrieval fails, we let pgx try without it
456-
// This allows fallback to other authentication methods
457-
}
458-
459-
return config, nil
460-
}
461-
462402
// testDatabaseConnection tests the database connection and returns appropriate exit codes
463-
func testDatabaseConnection(connectionString string, timeout time.Duration, service api.Service, cmd *cobra.Command) error {
403+
func testDatabaseConnection(ctx context.Context, connectionString string, timeout time.Duration, cmd *cobra.Command) error {
464404
// Create context with timeout if specified
465-
var ctx context.Context
466405
var cancel context.CancelFunc
467-
468406
if timeout > 0 {
469-
ctx, cancel = context.WithTimeout(context.Background(), timeout)
407+
ctx, cancel = context.WithTimeout(ctx, timeout)
470408
defer cancel()
471-
} else {
472-
ctx = context.Background()
473-
}
474-
475-
// Build connection config with proper password handling
476-
config, err := buildConnectionConfig(connectionString, service)
477-
if err != nil {
478-
fmt.Fprintf(cmd.ErrOrStderr(), "Failed to build connection config: %v\n", err)
479-
return exitWithCode(ExitInvalidParameters, err)
480409
}
481410

482411
// Attempt to connect to the database
483-
conn, err := pgx.ConnectConfig(ctx, config)
412+
// The connection string already includes the password (if available) thanks to PasswordOptional mode
413+
conn, err := pgx.Connect(ctx, connectionString)
484414
if err != nil {
485415
// Determine the appropriate exit code based on error type
486416
if isContextDeadlineExceeded(err) {

0 commit comments

Comments
 (0)