Skip to content

Commit ebeacac

Browse files
Add service_start and service_stop MCP tools (#104)
1 parent e121283 commit ebeacac

25 files changed

Lines changed: 715 additions & 511 deletions

CLAUDE.md

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,14 @@ func processData(cfg *config.Config) {
171171
When implementing or updating functionality:
172172

173173
1. **Keep CLI commands and MCP tools in sync** - When updating a CLI command, check if there's a corresponding MCP tool and apply the same changes to keep them aligned. Examples:
174-
- `tiger service list` command → `tiger_service_list` MCP tool
175-
- `tiger service create` command → `tiger_service_create` MCP tool
174+
- `tiger service list` command → `service_list` MCP tool
175+
- `tiger service create` command → `service_create` MCP tool
176176

177177
2. **Check for intentional differences** - Some discrepancies between CLI and MCP are intentional (e.g., different default behaviors, different output formats). Before making changes to sync them, ask whether the difference is intentional. Document intentional differences in code comments.
178178

179179
3. **Share code between CLI and MCP** - Code that needs to be used by both CLI commands and MCP tools should be moved to a shared package (not in `internal/tiger/cmd` or `internal/tiger/mcp`). Current examples:
180-
- `internal/tiger/util/` - Shared utility functions
181-
- `internal/tiger/password/` - Password storage logic used by both CLI and MCP
180+
- `internal/tiger/common/` - Shared business logic, password storage, wait operations, error handling, and other utilities that have dependencies on config/api packages
181+
- `internal/tiger/util/` - Small utility functions with minimal dependencies (formatting, validation, etc.)
182182
- `internal/tiger/api/` - API client used by both
183183

184184
### Documentation Synchronization
@@ -268,7 +268,7 @@ Tiger CLI is a Go-based command-line interface for managing Tiger, the modern da
268268
- **Command Structure**: `internal/tiger/cmd/` - Cobra-based command definitions
269269
- `root.go` - Root command with global flags and configuration initialization
270270
- `auth.go` - Authentication commands (login, logout, status)
271-
- `service.go` - Service management commands (list, create, get, fork, delete, update-password)
271+
- `service.go` - Service management commands (list, create, get, fork, start, stop, delete, update-password)
272272
- `db.go` - Database operation commands (connection-string, connect, test-connection)
273273
- `config.go` - Configuration management commands (show, set, unset, reset)
274274
- `mcp.go` - MCP server commands (install, start)
@@ -278,10 +278,15 @@ Tiger CLI is a Go-based command-line interface for managing Tiger, the modern da
278278
- **API Client**: `internal/tiger/api/` - Generated OpenAPI client with mocks
279279
- **MCP Server**: `internal/tiger/mcp/` - Model Context Protocol server implementation
280280
- `server.go` - MCP server initialization, tool registration, and lifecycle management
281-
- `service_tools.go` - Service management tools (list, get, create, update-password)
281+
- `service_tools.go` - Service management tools (list, get, create, fork, start, stop, update-password)
282282
- `db_tools.go` - Database operation tools (execute-query)
283283
- `proxy.go` - Proxy client that forwards tools/resources/prompts from remote docs MCP server
284-
- **Password Storage**: `internal/tiger/password/` - Secure password storage utilities
284+
- **Common Package**: `internal/tiger/common/` - Shared business logic used by both CLI and MCP
285+
- Password storage utilities (keyring, pgpass, validation)
286+
- Wait operations and polling logic (WaitForService)
287+
- Error handling and exit code utilities
288+
- Service detail conversion helpers
289+
- **Utilities**: `internal/tiger/util/` - Small utility functions with minimal dependencies
285290

286291
### Configuration System
287292

@@ -304,7 +309,7 @@ The Tiger MCP server provides AI assistants with programmatic access to Tiger re
304309
**Two Types of Tools:**
305310

306311
1. **Direct Tiger Tools** - Native tools for Tiger operations
307-
- `service_tools.go` - Service management (list, get, create, update-password)
312+
- `service_tools.go` - Service management (list, get, create, fork, start, stop, update-password)
308313
- `db_tools.go` - Database operations (execute-query)
309314
2. **Proxied Documentation Tools** (`proxy.go`) - Tools forwarded from a remote docs MCP server (see `proxy.go` for implementation)
310315

@@ -351,7 +356,7 @@ func (ServiceCreateInput) Schema() *jsonschema.Schema {
351356
3. **Register tool with enhanced schema**:
352357
```go
353358
mcp.AddTool(s.mcpServer, &mcp.Tool{
354-
Name: "tiger_service_create",
359+
Name: "service_create",
355360
Description: `Detailed multi-line description...`,
356361
InputSchema: ServiceCreateInput{}.Schema(), // Uses our enhanced schema
357362
}, s.handleServiceCreate)
@@ -400,9 +405,9 @@ tiger-cli/
400405
│ ├── config/ # Configuration management
401406
│ ├── logging/ # Structured logging utilities
402407
│ ├── mcp/ # MCP server implementation
403-
│ ├── password/ # Password storage utilities
408+
│ ├── common/ # Shared business logic (password storage, wait ops, error handling)
404409
│ ├── cmd/ # CLI commands (Cobra)
405-
│ └── util/ # Shared utilities
410+
│ └── util/ # Small utility functions with minimal dependencies
406411
├── docs/ # Documentation
407412
│ └── development.md # Development guide (building, testing, contributing)
408413
├── specs/ # CLI specifications and API documentation
@@ -485,6 +490,8 @@ buildRootCmd() → Complete CLI with all commands and flags
485490
│ ├── buildServiceGetCmd()
486491
│ ├── buildServiceCreateCmd()
487492
│ ├── buildServiceForkCmd()
493+
│ ├── buildServiceStartCmd()
494+
│ ├── buildServiceStopCmd()
488495
│ ├── buildServiceDeleteCmd()
489496
│ └── buildServiceUpdatePasswordCmd()
490497
├── buildDbCmd()

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ Tiger CLI provides the following commands:
8888
- `create` - Create a new service
8989
- `get` - Show detailed service information (aliases: `describe`, `show`)
9090
- `fork` - Fork an existing service
91+
- `start` - Start a stopped service
92+
- `stop` - Stop a running service
9193
- `delete` - Delete a service
9294
- `update-password` - Update service master password
9395
- `tiger db` - Database operations
@@ -167,6 +169,8 @@ The MCP server exposes the following tools to AI assistants:
167169
- `service_get` - Get detailed information about a specific service
168170
- `service_create` - Create new database services with configurable resources
169171
- `service_fork` - Fork an existing database service to create an independent copy
172+
- `service_start` - Start a stopped database service
173+
- `service_stop` - Stop a running database service
170174
- `service_update_password` - Update the master password for a service
171175

172176
**Database Operations:**

internal/tiger/cmd/db.go

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import (
1717
"golang.org/x/term"
1818

1919
"github.com/timescale/tiger-cli/internal/tiger/api"
20+
"github.com/timescale/tiger-cli/internal/tiger/common"
2021
"github.com/timescale/tiger-cli/internal/tiger/config"
21-
"github.com/timescale/tiger-cli/internal/tiger/password"
2222
"github.com/timescale/tiger-cli/internal/tiger/util"
2323
)
2424

@@ -84,7 +84,7 @@ Examples:
8484
return err
8585
}
8686

87-
details, err := password.GetConnectionDetails(service, password.ConnectionDetailsOptions{
87+
details, err := common.GetConnectionDetails(service, common.ConnectionDetailsOptions{
8888
Pooled: dbConnectionStringPooled,
8989
Role: dbConnectionStringRole,
9090
WithPassword: dbConnectionStringWithPassword,
@@ -170,7 +170,7 @@ Examples:
170170
return fmt.Errorf("psql client not found. Please install PostgreSQL client tools")
171171
}
172172

173-
details, err := password.GetConnectionDetails(service, password.ConnectionDetailsOptions{
173+
details, err := common.GetConnectionDetails(service, common.ConnectionDetailsOptions{
174174
Pooled: dbConnectPooled,
175175
Role: dbConnectRole,
176176
})
@@ -234,26 +234,26 @@ Examples:
234234
RunE: func(cmd *cobra.Command, args []string) error {
235235
service, err := getServiceDetails(cmd, args)
236236
if err != nil {
237-
return exitWithCode(ExitInvalidParameters, err)
237+
return common.ExitWithCode(common.ExitInvalidParameters, err)
238238
}
239239

240240
// Build connection string for testing with password (if available)
241-
details, err := password.GetConnectionDetails(service, password.ConnectionDetailsOptions{
241+
details, err := common.GetConnectionDetails(service, common.ConnectionDetailsOptions{
242242
Pooled: dbTestConnectionPooled,
243243
Role: dbTestConnectionRole,
244244
WithPassword: true,
245245
})
246246
if err != nil {
247-
return exitWithCode(ExitInvalidParameters, fmt.Errorf("failed to build connection string: %w", err))
247+
return common.ExitWithCode(common.ExitInvalidParameters, fmt.Errorf("failed to build connection string: %w", err))
248248
}
249249

250250
if dbTestConnectionPooled && !details.IsPooler {
251-
return exitWithCode(ExitInvalidParameters, fmt.Errorf("connection pooler not available for this service"))
251+
return common.ExitWithCode(common.ExitInvalidParameters, fmt.Errorf("connection pooler not available for this service"))
252252
}
253253

254254
// Validate timeout (Cobra handles parsing automatically)
255255
if dbTestConnectionTimeout < 0 {
256-
return exitWithCode(ExitInvalidParameters, fmt.Errorf("timeout must be positive or zero, got %v", dbTestConnectionTimeout))
256+
return common.ExitWithCode(common.ExitInvalidParameters, fmt.Errorf("timeout must be positive or zero, got %v", dbTestConnectionTimeout))
257257
}
258258

259259
// Test the connection
@@ -344,7 +344,7 @@ Examples:
344344
}
345345

346346
// Save password using configured storage
347-
storage := password.GetPasswordStorage()
347+
storage := common.GetPasswordStorage()
348348
if err := storage.Save(service, passwordToSave, dbSavePasswordRole); err != nil {
349349
return fmt.Errorf("failed to save password: %w", err)
350350
}
@@ -674,7 +674,7 @@ PostgreSQL Configuration Parameters That May Be Set:
674674
}
675675

676676
// Build connection string
677-
details, err := password.GetConnectionDetails(service, password.ConnectionDetailsOptions{
677+
details, err := common.GetConnectionDetails(service, common.ConnectionDetailsOptions{
678678
Pooled: false,
679679
Role: "tsdbadmin", // Use admin role to create new roles
680680
WithPassword: true,
@@ -699,7 +699,7 @@ PostgreSQL Configuration Parameters That May Be Set:
699699
}
700700

701701
// Save password to storage with the new role name
702-
result, err := password.SavePasswordWithResult(service, rolePassword, roleName)
702+
result, err := common.SavePasswordWithResult(service, rolePassword, roleName)
703703
if err != nil {
704704
fmt.Fprintf(cmd.ErrOrStderr(), "⚠️ Warning: %s\n", result.Message)
705705
} else if !result.Success {
@@ -771,7 +771,7 @@ func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) {
771771
// Get API key and project ID for authentication
772772
apiKey, projectID, err := getCredentialsForDB()
773773
if err != nil {
774-
return api.Service{}, exitWithCode(ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
774+
return api.Service{}, common.ExitWithCode(common.ExitAuthenticationError, fmt.Errorf("authentication required: %w. Please run 'tiger auth login'", err))
775775
}
776776

777777
// Create API client
@@ -791,7 +791,7 @@ func getServiceDetails(cmd *cobra.Command, args []string) (api.Service, error) {
791791

792792
// Handle API response
793793
if resp.StatusCode() != 200 {
794-
return api.Service{}, exitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
794+
return api.Service{}, common.ExitWithErrorFromStatusCode(resp.StatusCode(), resp.JSON4XX)
795795
}
796796

797797
if resp.JSON200 == nil {
@@ -847,8 +847,8 @@ func buildPsqlCommand(connectionString, psqlPath string, additionalFlags []strin
847847

848848
// Only set PGPASSWORD for keyring storage method
849849
// pgpass storage relies on psql automatically reading ~/.pgpass file
850-
storage := password.GetPasswordStorage()
851-
if _, isKeyring := storage.(*password.KeyringStorage); isKeyring {
850+
storage := common.GetPasswordStorage()
851+
if _, isKeyring := storage.(*common.KeyringStorage); isKeyring {
852852
if password, err := storage.Get(service, role); err == nil && password != "" {
853853
// Set PGPASSWORD environment variable for psql when using keyring
854854
psqlCmd.Env = append(os.Environ(), "PGPASSWORD="+password)
@@ -876,17 +876,17 @@ func testDatabaseConnection(ctx context.Context, connectionString string, timeou
876876
// Determine the appropriate exit code based on error type
877877
if isContextDeadlineExceeded(err) {
878878
fmt.Fprintf(cmd.ErrOrStderr(), "Connection timeout after %v\n", timeout)
879-
return exitWithCode(ExitTimeout, err) // Connection timeout
879+
return common.ExitWithCode(common.ExitTimeout, err) // Connection timeout
880880
}
881881

882882
// Check if it's a connection rejection vs unreachable
883883
if isConnectionRejected(err) {
884884
fmt.Fprintf(cmd.ErrOrStderr(), "Connection rejected: %v\n", err)
885-
return exitWithCode(ExitGeneralError, err) // Server is rejecting connections
885+
return common.ExitWithCode(common.ExitGeneralError, err) // Server is rejecting connections
886886
}
887887

888888
fmt.Fprintf(cmd.ErrOrStderr(), "Connection failed: %v\n", err)
889-
return exitWithCode(2, err) // No response to connection attempt
889+
return common.ExitWithCode(2, err) // No response to connection attempt
890890
}
891891
defer conn.Close(ctx)
892892

@@ -896,17 +896,17 @@ func testDatabaseConnection(ctx context.Context, connectionString string, timeou
896896
// Determine the appropriate exit code based on error type
897897
if isContextDeadlineExceeded(err) {
898898
fmt.Fprintf(cmd.ErrOrStderr(), "Connection timeout after %v\n", timeout)
899-
return exitWithCode(ExitTimeout, err) // Connection timeout
899+
return common.ExitWithCode(common.ExitTimeout, err) // Connection timeout
900900
}
901901

902902
// Check if it's a connection rejection vs unreachable
903903
if isConnectionRejected(err) {
904904
fmt.Fprintf(cmd.ErrOrStderr(), "Connection rejected: %v\n", err)
905-
return exitWithCode(ExitGeneralError, err) // Server is rejecting connections
905+
return common.ExitWithCode(common.ExitGeneralError, err) // Server is rejecting connections
906906
}
907907

908908
fmt.Fprintf(cmd.ErrOrStderr(), "Connection failed: %v\n", err)
909-
return exitWithCode(2, err) // No response to connection attempt
909+
return common.ExitWithCode(2, err) // No response to connection attempt
910910
}
911911

912912
// Connection successful

0 commit comments

Comments
 (0)