From e1a47d11b1f53e133981770be1c6657c3d506710 Mon Sep 17 00:00:00 2001 From: leggetter Date: Fri, 27 Mar 2026 12:42:30 +0000 Subject: [PATCH 1/2] feat: accept connection name or ID in pause/unpause commands --- pkg/cmd/connection_pause.go | 11 +++++-- pkg/cmd/connection_unpause.go | 11 +++++-- pkg/gateway/mcp/tool_connections.go | 49 +++++++++++++++++++++++++---- pkg/gateway/mcp/tool_help.go | 2 +- pkg/gateway/mcp/tools.go | 2 +- 5 files changed, 61 insertions(+), 14 deletions(-) diff --git a/pkg/cmd/connection_pause.go b/pkg/cmd/connection_pause.go index 404e9c85..982566fb 100644 --- a/pkg/cmd/connection_pause.go +++ b/pkg/cmd/connection_pause.go @@ -17,7 +17,7 @@ func newConnectionPauseCmd() *connectionPauseCmd { cc := &connectionPauseCmd{} cc.cmd = &cobra.Command{ - Use: "pause ", + Use: "pause ", Args: validators.ExactArgs(1), Short: "Pause a connection temporarily", Long: `Pause a connection temporarily. @@ -26,7 +26,7 @@ The connection will queue incoming events until unpaused.`, RunE: cc.runConnectionPauseCmd, } cc.cmd.Annotations = map[string]string{ - "cli.arguments": `[{"name":"connection-id","type":"string","description":"Connection ID","required":true}]`, + "cli.arguments": `[{"name":"connection-id-or-name","type":"string","description":"Connection ID or name","required":true}]`, } return cc @@ -40,7 +40,12 @@ func (cc *connectionPauseCmd) runConnectionPauseCmd(cmd *cobra.Command, args []s client := Config.GetAPIClient() ctx := context.Background() - conn, err := client.PauseConnection(ctx, args[0]) + id, err := resolveConnectionID(ctx, client, args[0]) + if err != nil { + return err + } + + conn, err := client.PauseConnection(ctx, id) if err != nil { return fmt.Errorf("failed to pause connection: %w", err) } diff --git a/pkg/cmd/connection_unpause.go b/pkg/cmd/connection_unpause.go index 40b4d13e..41758ec6 100644 --- a/pkg/cmd/connection_unpause.go +++ b/pkg/cmd/connection_unpause.go @@ -17,7 +17,7 @@ func newConnectionUnpauseCmd() *connectionUnpauseCmd { cc := &connectionUnpauseCmd{} cc.cmd = &cobra.Command{ - Use: "unpause ", + Use: "unpause ", Args: validators.ExactArgs(1), Short: "Resume a paused connection", Long: `Resume a paused connection. @@ -26,7 +26,7 @@ The connection will start processing queued events.`, RunE: cc.runConnectionUnpauseCmd, } cc.cmd.Annotations = map[string]string{ - "cli.arguments": `[{"name":"connection-id","type":"string","description":"Connection ID","required":true}]`, + "cli.arguments": `[{"name":"connection-id-or-name","type":"string","description":"Connection ID or name","required":true}]`, } return cc @@ -40,7 +40,12 @@ func (cc *connectionUnpauseCmd) runConnectionUnpauseCmd(cmd *cobra.Command, args client := Config.GetAPIClient() ctx := context.Background() - conn, err := client.UnpauseConnection(ctx, args[0]) + id, err := resolveConnectionID(ctx, client, args[0]) + if err != nil { + return err + } + + conn, err := client.UnpauseConnection(ctx, id) if err != nil { return fmt.Errorf("failed to unpause connection: %w", err) } diff --git a/pkg/gateway/mcp/tool_connections.go b/pkg/gateway/mcp/tool_connections.go index 6d7c327e..211a1d18 100644 --- a/pkg/gateway/mcp/tool_connections.go +++ b/pkg/gateway/mcp/tool_connections.go @@ -3,6 +3,7 @@ package mcp import ( "context" "fmt" + "strings" mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp" @@ -71,9 +72,13 @@ func connectionsGet(ctx context.Context, client *hookdeck.Client, in input) (*mc } func connectionsPause(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) { - id := in.String("id") - if id == "" { - return ErrorResult("id is required for the pause action"), nil + idOrName := in.String("id") + if idOrName == "" { + return ErrorResult("id is required for the pause action (connection ID or name)"), nil + } + id, err := resolveMCPConnectionID(ctx, client, idOrName) + if err != nil { + return ErrorResult(err.Error()), nil } conn, err := client.PauseConnection(ctx, id) if err != nil { @@ -83,9 +88,13 @@ func connectionsPause(ctx context.Context, client *hookdeck.Client, in input) (* } func connectionsUnpause(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) { - id := in.String("id") - if id == "" { - return ErrorResult("id is required for the unpause action"), nil + idOrName := in.String("id") + if idOrName == "" { + return ErrorResult("id is required for the unpause action (connection ID or name)"), nil + } + id, err := resolveMCPConnectionID(ctx, client, idOrName) + if err != nil { + return ErrorResult(err.Error()), nil } conn, err := client.UnpauseConnection(ctx, id) if err != nil { @@ -93,3 +102,31 @@ func connectionsUnpause(ctx context.Context, client *hookdeck.Client, in input) } return JSONResult(conn) } + +// resolveMCPConnectionID resolves a connection ID or name to an ID. +// If the value looks like an ID (starts with conn_ or web_), it is returned as-is after +// verifying it exists; otherwise a name lookup is performed. +func resolveMCPConnectionID(ctx context.Context, client *hookdeck.Client, idOrName string) (string, error) { + if strings.HasPrefix(idOrName, "conn_") || strings.HasPrefix(idOrName, "web_") { + _, err := client.GetConnection(ctx, idOrName) + if err == nil { + return idOrName, nil + } + if !hookdeck.IsNotFoundError(err) { + return "", fmt.Errorf("failed to get connection: %w", err) + } + } + + params := map[string]string{"name": idOrName} + result, err := client.ListConnections(ctx, params) + if err != nil { + return "", fmt.Errorf("failed to lookup connection by name '%s': %w", idOrName, err) + } + if result.Pagination.Limit == 0 || len(result.Models) == 0 { + return "", fmt.Errorf("connection not found: '%s'", idOrName) + } + if len(result.Models) > 1 { + return "", fmt.Errorf("multiple connections found with name '%s', please use the connection ID instead", idOrName) + } + return result.Models[0].ID, nil +} diff --git a/pkg/gateway/mcp/tool_help.go b/pkg/gateway/mcp/tool_help.go index b27f5e90..92dcb819 100644 --- a/pkg/gateway/mcp/tool_help.go +++ b/pkg/gateway/mcp/tool_help.go @@ -73,7 +73,7 @@ Actions: Parameters: action (string, required) — list, get, pause, or unpause - id (string) — Required for get/pause/unpause + id (string) — Connection ID or name (required for get/pause/unpause) name (string) — Filter by name (list) source_id (string) — Filter by source (list) destination_id (string) — Filter by destination (list) diff --git a/pkg/gateway/mcp/tools.go b/pkg/gateway/mcp/tools.go index 4ae255fe..a7349598 100644 --- a/pkg/gateway/mcp/tools.go +++ b/pkg/gateway/mcp/tools.go @@ -36,7 +36,7 @@ func toolDefs(client *hookdeck.Client) []struct { Description: "Inspect connections (routes linking sources to destinations). List connections with filters, get details by ID, or pause/unpause a connection's delivery pipeline.", InputSchema: schema(map[string]prop{ "action": {Type: "string", Desc: "Action: list, get, pause, or unpause", Enum: []string{"list", "get", "pause", "unpause"}}, - "id": {Type: "string", Desc: "Connection ID (required for get/pause/unpause)"}, + "id": {Type: "string", Desc: "Connection ID or name (required for get/pause/unpause)"}, "name": {Type: "string", Desc: "Filter by name (list)"}, "source_id": {Type: "string", Desc: "Filter by source ID (list)"}, "destination_id": {Type: "string", Desc: "Filter by destination ID (list)"}, From 19a67d838761ac5a02fc19e83b1a7a39e5b9d084 Mon Sep 17 00:00:00 2001 From: Phil Leggetter Date: Fri, 27 Mar 2026 13:02:00 +0000 Subject: [PATCH 2/2] test(cli,mcp): finish connection name/ID resolution for issue #276 Completes the pause/unpause name-or-ID work from the parent commit and aligns MCP, tests, and user-facing copy. CLI - Document pause/unpause with examples (ID and name) in command Long text. - Add httptest coverage for resolveConnectionID: direct web_* ID, lookup by name via ListConnections, and web_* GET 404 falling back to name. Gateway MCP (hookdeck_connections) - Resolve connection ID or name for get, pause, and unpause using the same rules as the CLI (prefix probe, then list-by-name). - Surface API errors during resolution through TranslateAPIError so responses match other tools (e.g. authentication failures). - Validation errors: id or name is required for get/pause/unpause. - Refresh tool description, static help, and JSON schema text for get by name. Tests - MCP server tests: stub GET /connections/{id} before pause/unpause; include pagination limit in list mocks so name resolution succeeds; add get/pause/unpause-by-name cases; update missing-parameter assertions. - Acceptance: TestConnectionPauseUnpauseByName exercises pause/unpause by name against the real API and checks paused_at via connection get. Refs: https://github.com/hookdeck/hookdeck-cli/issues/276 Made-with: Cursor --- pkg/cmd/connection_get_resolve_test.go | 98 ++++++++++++++++++++++++++ pkg/cmd/connection_pause.go | 9 ++- pkg/cmd/connection_unpause.go | 9 ++- pkg/gateway/mcp/server_test.go | 73 +++++++++++++++++-- pkg/gateway/mcp/tool_connections.go | 19 +++-- pkg/gateway/mcp/tool_help.go | 2 +- pkg/gateway/mcp/tools.go | 2 +- test/acceptance/connection_test.go | 59 ++++++++++++++++ 8 files changed, 255 insertions(+), 16 deletions(-) create mode 100644 pkg/cmd/connection_get_resolve_test.go diff --git a/pkg/cmd/connection_get_resolve_test.go b/pkg/cmd/connection_get_resolve_test.go new file mode 100644 index 00000000..b87392a4 --- /dev/null +++ b/pkg/cmd/connection_get_resolve_test.go @@ -0,0 +1,98 @@ +package cmd + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/hookdeck/hookdeck-cli/pkg/hookdeck" +) + +func TestResolveConnectionID_ByIDPrefix(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + mux.HandleFunc(hookdeck.APIPathPrefix+"/connections/web_resolve1", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + _ = json.NewEncoder(w).Encode(map[string]any{"id": "web_resolve1", "team_id": "tm_1"}) + }) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + + baseURL, err := url.Parse(srv.URL) + require.NoError(t, err) + client := &hookdeck.Client{BaseURL: baseURL, APIKey: "k"} + + id, err := resolveConnectionID(context.Background(), client, "web_resolve1") + require.NoError(t, err) + assert.Equal(t, "web_resolve1", id) +} + +func TestResolveConnectionID_ByName(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + mux.HandleFunc(hookdeck.APIPathPrefix+"/connections", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "method", http.StatusMethodNotAllowed) + return + } + assert.Equal(t, "my-conn", r.URL.Query().Get("name")) + resp := map[string]any{ + "models": []map[string]any{{"id": "web_named", "team_id": "tm_1"}}, + "pagination": map[string]any{ + "limit": float64(100), + }, + } + _ = json.NewEncoder(w).Encode(resp) + }) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + + baseURL, err := url.Parse(srv.URL) + require.NoError(t, err) + client := &hookdeck.Client{BaseURL: baseURL, APIKey: "k"} + + id, err := resolveConnectionID(context.Background(), client, "my-conn") + require.NoError(t, err) + assert.Equal(t, "web_named", id) +} + +func TestResolveConnectionID_WebPrefix404FallsBackToNameLookup(t *testing.T) { + t.Parallel() + + mux := http.NewServeMux() + mux.HandleFunc(hookdeck.APIPathPrefix+"/connections/web_stale", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + _ = json.NewEncoder(w).Encode(map[string]any{"message": "not found"}) + }) + mux.HandleFunc(hookdeck.APIPathPrefix+"/connections", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "web_stale", r.URL.Query().Get("name")) + resp := map[string]any{ + "models": []map[string]any{{"id": "web_real", "team_id": "tm_1"}}, + "pagination": map[string]any{ + "limit": float64(100), + }, + } + _ = json.NewEncoder(w).Encode(resp) + }) + srv := httptest.NewServer(mux) + t.Cleanup(srv.Close) + + baseURL, err := url.Parse(srv.URL) + require.NoError(t, err) + client := &hookdeck.Client{BaseURL: baseURL, APIKey: "k"} + + id, err := resolveConnectionID(context.Background(), client, "web_stale") + require.NoError(t, err) + assert.Equal(t, "web_real", id) +} diff --git a/pkg/cmd/connection_pause.go b/pkg/cmd/connection_pause.go index 982566fb..010e5bed 100644 --- a/pkg/cmd/connection_pause.go +++ b/pkg/cmd/connection_pause.go @@ -22,7 +22,14 @@ func newConnectionPauseCmd() *connectionPauseCmd { Short: "Pause a connection temporarily", Long: `Pause a connection temporarily. -The connection will queue incoming events until unpaused.`, +The connection will queue incoming events until unpaused. + +Examples: + # Pause by connection ID + hookdeck gateway connection pause web_abc123 + + # Pause by connection name + hookdeck gateway connection pause my-connection`, RunE: cc.runConnectionPauseCmd, } cc.cmd.Annotations = map[string]string{ diff --git a/pkg/cmd/connection_unpause.go b/pkg/cmd/connection_unpause.go index 41758ec6..9f826efa 100644 --- a/pkg/cmd/connection_unpause.go +++ b/pkg/cmd/connection_unpause.go @@ -22,7 +22,14 @@ func newConnectionUnpauseCmd() *connectionUnpauseCmd { Short: "Resume a paused connection", Long: `Resume a paused connection. -The connection will start processing queued events.`, +The connection will start processing queued events. + +Examples: + # Unpause by connection ID + hookdeck gateway connection unpause web_abc123 + + # Unpause by connection name + hookdeck gateway connection unpause my-connection`, RunE: cc.runConnectionUnpauseCmd, } cc.cmd.Annotations = map[string]string{ diff --git a/pkg/gateway/mcp/server_test.go b/pkg/gateway/mcp/server_test.go index 8488f78e..1fafe5f5 100644 --- a/pkg/gateway/mcp/server_test.go +++ b/pkg/gateway/mcp/server_test.go @@ -82,8 +82,9 @@ func callTool(t *testing.T, session *mcpsdk.ClientSession, name string, args map // listResponse returns a standard paginated API response. func listResponse(models ...map[string]any) map[string]any { return map[string]any{ - "models": models, - "pagination": map[string]any{}, + "models": models, + // limit must be non-zero so connection name resolution (ListConnections) is not treated as empty + "pagination": map[string]any{"limit": 100}, } } @@ -377,16 +378,38 @@ func TestConnectionsGet_Success(t *testing.T) { assert.Contains(t, textContent(t, result), "web_conn1") } +func TestConnectionsGet_ByName(t *testing.T) { + session := mockAPIWithClient(t, map[string]http.HandlerFunc{ + "/2025-07-01/connections": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "stripe-to-backend", r.URL.Query().Get("name")) + json.NewEncoder(w).Encode(listResponse(map[string]any{"id": "web_conn1", "name": "stripe-to-backend"})) + }, + "/2025-07-01/connections/web_conn1": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + json.NewEncoder(w).Encode(map[string]any{"id": "web_conn1", "name": "stripe-to-backend"}) + }, + }) + + result := callTool(t, session, "hookdeck_connections", map[string]any{"action": "get", "id": "stripe-to-backend"}) + assert.False(t, result.IsError) + assert.Contains(t, textContent(t, result), "web_conn1") +} + func TestConnectionsGet_MissingID(t *testing.T) { client := newTestClient("https://api.hookdeck.com", "test-key") session := connectInMemory(t, client) result := callTool(t, session, "hookdeck_connections", map[string]any{"action": "get"}) assert.True(t, result.IsError) - assert.Contains(t, textContent(t, result), "id is required") + assert.Contains(t, textContent(t, result), "id or name is required") } func TestConnectionsPause_Success(t *testing.T) { session := mockAPIWithClient(t, map[string]http.HandlerFunc{ + "/2025-07-01/connections/web_conn1": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + json.NewEncoder(w).Encode(map[string]any{"id": "web_conn1", "name": "stripe-to-backend"}) + }, "/2025-07-01/connections/web_conn1/pause": func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) json.NewEncoder(w).Encode(map[string]any{"id": "web_conn1", "paused_at": "2025-01-01T00:00:00Z"}) @@ -398,16 +421,38 @@ func TestConnectionsPause_Success(t *testing.T) { assert.Contains(t, textContent(t, result), "web_conn1") } +func TestConnectionsPause_ByName(t *testing.T) { + session := mockAPIWithClient(t, map[string]http.HandlerFunc{ + "/2025-07-01/connections": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "stripe-to-backend", r.URL.Query().Get("name")) + json.NewEncoder(w).Encode(listResponse(map[string]any{"id": "web_conn1", "name": "stripe-to-backend"})) + }, + "/2025-07-01/connections/web_conn1/pause": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + json.NewEncoder(w).Encode(map[string]any{"id": "web_conn1", "paused_at": "2025-01-01T00:00:00Z"}) + }, + }) + + result := callTool(t, session, "hookdeck_connections", map[string]any{"action": "pause", "id": "stripe-to-backend"}) + assert.False(t, result.IsError) + assert.Contains(t, textContent(t, result), "web_conn1") +} + func TestConnectionsPause_MissingID(t *testing.T) { client := newTestClient("https://api.hookdeck.com", "test-key") session := connectInMemory(t, client) result := callTool(t, session, "hookdeck_connections", map[string]any{"action": "pause"}) assert.True(t, result.IsError) - assert.Contains(t, textContent(t, result), "id is required") + assert.Contains(t, textContent(t, result), "id or name is required") } func TestConnectionsUnpause_Success(t *testing.T) { session := mockAPIWithClient(t, map[string]http.HandlerFunc{ + "/2025-07-01/connections/web_conn1": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + json.NewEncoder(w).Encode(map[string]any{"id": "web_conn1", "name": "stripe-to-backend"}) + }, "/2025-07-01/connections/web_conn1/unpause": func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "PUT", r.Method) json.NewEncoder(w).Encode(map[string]any{"id": "web_conn1"}) @@ -419,12 +464,30 @@ func TestConnectionsUnpause_Success(t *testing.T) { assert.Contains(t, textContent(t, result), "web_conn1") } +func TestConnectionsUnpause_ByName(t *testing.T) { + session := mockAPIWithClient(t, map[string]http.HandlerFunc{ + "/2025-07-01/connections": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "GET", r.Method) + assert.Equal(t, "stripe-to-backend", r.URL.Query().Get("name")) + json.NewEncoder(w).Encode(listResponse(map[string]any{"id": "web_conn1", "name": "stripe-to-backend"})) + }, + "/2025-07-01/connections/web_conn1/unpause": func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "PUT", r.Method) + json.NewEncoder(w).Encode(map[string]any{"id": "web_conn1"}) + }, + }) + + result := callTool(t, session, "hookdeck_connections", map[string]any{"action": "unpause", "id": "stripe-to-backend"}) + assert.False(t, result.IsError) + assert.Contains(t, textContent(t, result), "web_conn1") +} + func TestConnectionsUnpause_MissingID(t *testing.T) { client := newTestClient("https://api.hookdeck.com", "test-key") session := connectInMemory(t, client) result := callTool(t, session, "hookdeck_connections", map[string]any{"action": "unpause"}) assert.True(t, result.IsError) - assert.Contains(t, textContent(t, result), "id is required") + assert.Contains(t, textContent(t, result), "id or name is required") } func TestConnectionsTool_UnknownAction(t *testing.T) { diff --git a/pkg/gateway/mcp/tool_connections.go b/pkg/gateway/mcp/tool_connections.go index 211a1d18..d2658f0d 100644 --- a/pkg/gateway/mcp/tool_connections.go +++ b/pkg/gateway/mcp/tool_connections.go @@ -2,6 +2,7 @@ package mcp import ( "context" + "errors" "fmt" "strings" @@ -60,9 +61,13 @@ func connectionsList(ctx context.Context, client *hookdeck.Client, in input) (*m } func connectionsGet(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) { - id := in.String("id") - if id == "" { - return ErrorResult("id is required for the get action"), nil + idOrName := in.String("id") + if idOrName == "" { + return ErrorResult("id or name is required for the get action"), nil + } + id, err := resolveMCPConnectionID(ctx, client, idOrName) + if err != nil { + return ErrorResult(err.Error()), nil } conn, err := client.GetConnection(ctx, id) if err != nil { @@ -74,7 +79,7 @@ func connectionsGet(ctx context.Context, client *hookdeck.Client, in input) (*mc func connectionsPause(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) { idOrName := in.String("id") if idOrName == "" { - return ErrorResult("id is required for the pause action (connection ID or name)"), nil + return ErrorResult("id or name is required for the pause action"), nil } id, err := resolveMCPConnectionID(ctx, client, idOrName) if err != nil { @@ -90,7 +95,7 @@ func connectionsPause(ctx context.Context, client *hookdeck.Client, in input) (* func connectionsUnpause(ctx context.Context, client *hookdeck.Client, in input) (*mcpsdk.CallToolResult, error) { idOrName := in.String("id") if idOrName == "" { - return ErrorResult("id is required for the unpause action (connection ID or name)"), nil + return ErrorResult("id or name is required for the unpause action"), nil } id, err := resolveMCPConnectionID(ctx, client, idOrName) if err != nil { @@ -113,14 +118,14 @@ func resolveMCPConnectionID(ctx context.Context, client *hookdeck.Client, idOrNa return idOrName, nil } if !hookdeck.IsNotFoundError(err) { - return "", fmt.Errorf("failed to get connection: %w", err) + return "", errors.New(TranslateAPIError(err)) } } params := map[string]string{"name": idOrName} result, err := client.ListConnections(ctx, params) if err != nil { - return "", fmt.Errorf("failed to lookup connection by name '%s': %w", idOrName, err) + return "", errors.New(TranslateAPIError(err)) } if result.Pagination.Limit == 0 || len(result.Models) == 0 { return "", fmt.Errorf("connection not found: '%s'", idOrName) diff --git a/pkg/gateway/mcp/tool_help.go b/pkg/gateway/mcp/tool_help.go index 92dcb819..e455008f 100644 --- a/pkg/gateway/mcp/tool_help.go +++ b/pkg/gateway/mcp/tool_help.go @@ -67,7 +67,7 @@ Parameters: Actions: list — List connections with optional filters - get — Get a single connection by ID + get — Get a single connection by ID or name pause — Pause a connection (stops event delivery) unpause — Resume a paused connection diff --git a/pkg/gateway/mcp/tools.go b/pkg/gateway/mcp/tools.go index a7349598..40e89c7f 100644 --- a/pkg/gateway/mcp/tools.go +++ b/pkg/gateway/mcp/tools.go @@ -33,7 +33,7 @@ func toolDefs(client *hookdeck.Client) []struct { { tool: &mcpsdk.Tool{ Name: "hookdeck_connections", - Description: "Inspect connections (routes linking sources to destinations). List connections with filters, get details by ID, or pause/unpause a connection's delivery pipeline.", + Description: "Inspect connections (routes linking sources to destinations). List connections with filters, get details by ID or name, or pause/unpause a connection's delivery pipeline.", InputSchema: schema(map[string]prop{ "action": {Type: "string", Desc: "Action: list, get, pause, or unpause", Enum: []string{"list", "get", "pause", "unpause"}}, "id": {Type: "string", Desc: "Connection ID or name (required for get/pause/unpause)"}, diff --git a/test/acceptance/connection_test.go b/test/acceptance/connection_test.go index d05c91f8..79257b86 100644 --- a/test/acceptance/connection_test.go +++ b/test/acceptance/connection_test.go @@ -115,6 +115,65 @@ func TestConnectionGetByName(t *testing.T) { t.Logf("Successfully tested connection get by both ID (%s) and name (%s)", createResp.ID, connName) } +// TestConnectionPauseUnpauseByName tests pause and unpause using connection name (same resolution as get). +func TestConnectionPauseUnpauseByName(t *testing.T) { + if testing.Short() { + t.Skip("Skipping acceptance test in short mode") + } + + cli := NewCLIRunner(t) + timestamp := generateTimestamp() + + connName := "test-pause-by-name-" + timestamp + sourceName := "test-src-pause-" + timestamp + destName := "test-dst-pause-" + timestamp + + var createResp Connection + err := cli.RunJSON(&createResp, + "gateway", "connection", "create", + "--name", connName, + "--source-name", sourceName, + "--source-type", "WEBHOOK", + "--destination-name", destName, + "--destination-type", "CLI", + "--destination-cli-path", "/webhooks", + ) + require.NoError(t, err, "Should create test connection") + require.NotEmpty(t, createResp.ID, "Connection should have an ID") + + t.Cleanup(func() { + deleteConnection(t, cli, createResp.ID) + }) + + // Pause by name + stdout, stderr, err := cli.Run("gateway", "connection", "pause", connName) + require.NoError(t, err, "pause by name should succeed: stderr=%s", stderr) + assert.Contains(t, stdout+stderr, "paused", "output should indicate paused state") + + var afterPause map[string]interface{} + err = cli.RunJSON(&afterPause, "gateway", "connection", "get", connName) + require.NoError(t, err, "get after pause should succeed") + if v, ok := afterPause["paused_at"]; ok && v != nil { + assert.NotNil(t, v, "paused_at should be set when connection is paused") + } else { + t.Fatal("expected paused_at in get JSON after pause") + } + + // Unpause by name + stdout, stderr, err = cli.Run("gateway", "connection", "unpause", connName) + require.NoError(t, err, "unpause by name should succeed: stderr=%s", stderr) + assert.Contains(t, stdout+stderr, "unpaused", "output should indicate unpaused state") + + var afterUnpause map[string]interface{} + err = cli.RunJSON(&afterUnpause, "gateway", "connection", "get", connName) + require.NoError(t, err, "get after unpause should succeed") + if v, ok := afterUnpause["paused_at"]; ok && v != nil { + t.Fatalf("expected paused_at cleared after unpause, got %v", v) + } + + t.Logf("Successfully paused and unpaused connection by name: %s", connName) +} + // TestConnectionGetNotFound tests error handling for non-existent connections func TestConnectionGetNotFound(t *testing.T) { if testing.Short() {