Skip to content

Commit a965637

Browse files
leggetterclaude
andcommitted
Merge Gateway MCP write mode and the gateway_ tool rename into v3.0.0
Brings the two workstreams onto one branch for a single 3.0.0 release: Outpost CLI support and MCP server, plus Gateway MCP write mode behind --allow-write and the hookdeck_ -> gateway_ product tool rename. The branches combined without textual conflict apart from README.md, but the Outpost coverage checklist referenced the package-private actionSet type that the Gateway work lifted into mcpcore. Git could not see that — a clean merge that did not compile — so the checklist now reads mcpcore.ActionSet. It is the test that fails when an action ships without a successful-call test, so losing it would have quietly reopened the gap it exists to close. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Wajsob136PyRh6nc92w5L3
2 parents 38e12ee + c18a1d7 commit a965637

40 files changed

Lines changed: 3585 additions & 1166 deletions

‎README.md‎

Lines changed: 50 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -614,55 +614,81 @@ Claude Desktop (`claude_desktop_config.json`):
614614

615615
The client starts `hookdeck gateway mcp` as a stdio subprocess. If you haven't authenticated yet, the `hookdeck_login` tool is available to log in via the browser.
616616

617+
#### Read-only by default
618+
619+
The server starts read-only. Tools advertise only the actions that read data, so an agent is never offered an action it cannot perform. Pass `--allow-write` (or set `HOOKDECK_MCP_ALLOW_WRITE=true`) to enable creating, changing and deleting:
620+
621+
```json
622+
{
623+
"mcpServers": {
624+
"hookdeck": {
625+
"command": "hookdeck",
626+
"args": ["gateway", "mcp", "--allow-write"]
627+
}
628+
}
629+
}
630+
```
631+
632+
`--read-only` is accepted explicitly and wins if both are passed.
633+
634+
Pausing and unpausing a connection are available in **both** modes. Read-only is the mode incidents get investigated in, and stopping a misbehaving connection is the natural end of an investigation; both are reversible, and pausing buffers delivery rather than dropping events.
635+
617636
#### Available tools
618637

619-
| Tool | Description |
620-
|------|-------------|
621-
| `hookdeck_projects` | List projects or switch the active project for this session |
622-
| `hookdeck_connections` | Inspect connections and control delivery flow (list, get, pause, unpause) |
623-
| `hookdeck_sources` | Inspect inbound sources (HTTP endpoints that receive events) |
624-
| `hookdeck_destinations` | Inspect delivery destinations (HTTP endpoints where events are sent) |
625-
| `hookdeck_transformations` | Inspect JavaScript transformations applied to event payloads |
626-
| `hookdeck_requests` | Query inbound requests — list, get details, raw body, linked events |
627-
| `hookdeck_events` | Query processed events — list, get details, raw payload body |
628-
| `hookdeck_attempts` | Query delivery attempts — retry history, response codes, errors |
629-
| `hookdeck_issues` | Inspect aggregated failure signals (delivery failures, transform errors, backpressure) |
630-
| `hookdeck_metrics` | Query aggregate metrics — counts, failure rates, queue depth over time |
631-
| `hookdeck_help` | Discover available tools and their actions |
632-
633-
`hookdeck_events` and `hookdeck_requests` **list** actions support the same filters as `hookdeck gateway event list` and `hookdeck gateway request list` — including payload search (`body`, `headers`, `parsed_query`, `path`) and date windows via `*_after` / `*_before` (ISO 8601; maps to API `field[gte]` / `field[lte]`). See `hookdeck_help` with topic `hookdeck_events` or `hookdeck_requests` for the full parameter list.
638+
Product tools are prefixed `gateway_`. Signing in and switching project are Hookdeck operations rather than Event Gateway ones, so they keep the platform `hookdeck_` prefix and are shared with `hookdeck outpost mcp`.
639+
640+
| Tool | Read actions | Added by `--allow-write` |
641+
|------|--------------|--------------------------|
642+
| `hookdeck_projects` | list, use | — |
643+
| `hookdeck_login` | (sign in) | — |
644+
| `gateway_connections` | list, get, pause, unpause | create, upsert, update, delete, enable, disable |
645+
| `gateway_sources` | list, get | create, upsert, update, delete, enable, disable |
646+
| `gateway_destinations` | list, get | create, upsert, update, delete, enable, disable |
647+
| `gateway_transformations` | list, get | create, upsert, update, delete, run |
648+
| `gateway_requests` | list, get, raw_body, events, ignored_events | retry |
649+
| `gateway_events` | list, get, raw_body | retry, cancel, mute |
650+
| `gateway_attempts` | list, get | — |
651+
| `gateway_issues` | list, get | update, dismiss |
652+
| `gateway_metrics` | events, requests, attempts, transformations | — |
653+
| `gateway_help` | overview, per-tool topics | — |
654+
655+
`transformations run` executes code without storing anything, but it is gated as a write: a read-only session should not be able to run caller-supplied code.
656+
657+
`gateway_events` and `gateway_requests` **list** actions support the same filters as `hookdeck gateway event list` and `hookdeck gateway request list` — including payload search (`body`, `headers`, `parsed_query`, `path`) and date windows via `*_after` / `*_before` (ISO 8601; maps to API `field[gte]` / `field[lte]`). See `gateway_help` with topic `gateway_events` or `gateway_requests` for the full parameter list.
658+
659+
`gateway_help` reports which mode the session is in and lists only the actions it can perform.
634660

635661
#### Example prompts
636662

637663
Once the MCP server is configured, you can ask your agent questions like:
638664

639665
```
640666
"Are any of my events failing right now?"
641-
→ Agent uses hookdeck_issues to list open issues, then hookdeck_events to inspect recent failures.
667+
→ Agent uses gateway_issues to list open issues, then gateway_events to inspect recent failures.
642668
643669
"Show me the last 10 events for my Stripe source and check if any failed."
644-
→ Agent uses hookdeck_sources to find the Stripe source, then hookdeck_events filtered by source and status.
670+
→ Agent uses gateway_sources to find the Stripe source, then gateway_events filtered by source and status.
645671
646672
"What's the error rate for my API destination over the last 24 hours?"
647-
→ Agent uses hookdeck_metrics with measures like failed_count and count, grouped by destination.
673+
→ Agent uses gateway_metrics with measures like failed_count and count, grouped by destination.
648674
649675
"Trace request req_abc123 — what events did it produce, and did they all deliver successfully?"
650-
→ Agent uses hookdeck_requests to get the request, then the events action to list generated events.
676+
→ Agent uses gateway_requests to get the request, then the events action to list generated events.
651677
652678
"Why is my checkout endpoint returning 500s? Show me the latest attempt details."
653-
→ Agent uses hookdeck_events filtered by status FAILED, then hookdeck_attempts to inspect delivery details.
679+
→ Agent uses gateway_events filtered by status FAILED, then gateway_attempts to inspect delivery details.
654680
655681
"Pause the connection between Stripe and my staging endpoint while I debug."
656-
→ Agent uses hookdeck_connections to find and pause the connection.
682+
→ Agent uses gateway_connections to find and pause the connection.
657683
658684
"Compare failure rates across all my destinations this week."
659-
→ Agent uses hookdeck_metrics with dimensions set to destination_id and measures like error_rate.
685+
→ Agent uses gateway_metrics with dimensions set to destination_id and measures like error_rate.
660686
661687
"Find Stripe charge.succeeded events from the last week."
662-
→ Agent uses hookdeck_events list with body filter {"type":"charge.succeeded"} and created_after / created_before ISO datetimes.
688+
→ Agent uses gateway_events list with body filter {"type":"charge.succeeded"} and created_after / created_before ISO datetimes.
663689
664690
"Show failed events that had delivery attempts in the last 24 hours."
665-
→ Agent uses hookdeck_events list with status FAILED and last_attempt_after set to yesterday's ISO datetime.
691+
→ Agent uses gateway_events list with status FAILED and last_attempt_after set to yesterday's ISO datetime.
666692
```
667693

668694
### Outpost

‎pkg/cmd/mcp.go‎

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"context"
5+
"os"
56

67
gatewaymcp "github.com/hookdeck/hookdeck-cli/pkg/gateway/mcp"
78
"github.com/hookdeck/hookdeck-cli/pkg/validators"
@@ -10,6 +11,9 @@ import (
1011

1112
type mcpCmd struct {
1213
cmd *cobra.Command
14+
15+
allowWrite bool
16+
readOnly bool
1317
}
1418

1519
func newMCPCmd() *mcpCmd {
@@ -24,6 +28,19 @@ The server exposes Hookdeck Event Gateway resources — connections, sources,
2428
destinations, events, requests, and more — as MCP tools that AI agents and
2529
LLM-based clients can invoke.
2630
31+
The server starts read-only: tools advertise only the actions that read data,
32+
so an agent is never offered an action it cannot perform. Pass --allow-write to
33+
enable creating, changing and deleting.
34+
35+
Pausing and unpausing a connection are available in both modes. Stopping a
36+
misbehaving connection is the natural end of an investigation, and both are
37+
reversible: pausing buffers delivery rather than dropping events.
38+
39+
Product tools are prefixed gateway_, so this server and 'hookdeck outpost mcp'
40+
can be configured in the same client. Signing in and switching project are
41+
Hookdeck operations rather than Event Gateway ones, so they keep the platform
42+
prefix: hookdeck_login and hookdeck_projects.
43+
2744
If the CLI is already authenticated, all tools are available immediately.
2845
If not, gateway MCP still starts: project selection is skipped until you
2946
authenticate, and hookdeck_login initiates browser-based sign-in. Protocol
@@ -32,13 +49,20 @@ the server runs go to stderr.
3249
3350
hookdeck_login stays registered after sign-in so you can call it with reauth: true
3451
to replace credentials (e.g. when project listing fails with a narrow API key).`),
35-
Example: ` # Start the MCP server (stdio transport)
52+
Example: ` # Start the MCP server, read-only (stdio transport)
3653
hookdeck gateway mcp
3754
55+
# Allow tools that change data
56+
hookdeck gateway mcp --allow-write
57+
3858
# Pipe a JSON-RPC initialize request for testing
3959
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test","version":"1.0"},"capabilities":{}}}' | hookdeck gateway mcp`,
4060
RunE: mc.runMCPCmd,
4161
}
62+
63+
addWriteModeFlags(mc.cmd, &mc.allowWrite, &mc.readOnly,
64+
"Enable tools that create, change or delete data.")
65+
4266
return mc
4367
}
4468

@@ -51,6 +75,18 @@ func (mc *mcpCmd) runMCPCmd(cmd *cobra.Command, args []string) error {
5175
// not yet authenticated. The MCP server handles this gracefully by
5276
// registering a hookdeck_login tool instead of crashing.
5377
client := Config.GetAPIClient()
54-
srv := gatewaymcp.NewServer(client, &Config)
78+
79+
writeEnabled := resolveAllowWrite(
80+
mc.allowWrite,
81+
cmd.Flags().Changed("allow-write"),
82+
mc.readOnly,
83+
os.Getenv(allowWriteEnvVar),
84+
)
85+
86+
srv := gatewaymcp.NewServer(gatewaymcp.ServerOptions{
87+
Client: client,
88+
Config: &Config,
89+
WriteEnabled: writeEnabled,
90+
})
5591
return srv.RunStdio(context.Background())
5692
}

‎pkg/cmd/mcp_write_mode.go‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cmd
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/spf13/cobra"
7+
)
8+
9+
// allowWriteEnvVar enables write actions without a flag, for MCP clients whose
10+
// config makes environment variables easier to set than arguments.
11+
//
12+
// It is shared by every MCP server the CLI starts: a user who wants write mode
13+
// should not have to learn a different variable per product.
14+
const allowWriteEnvVar = "HOOKDECK_MCP_ALLOW_WRITE"
15+
16+
// addWriteModeFlags registers the flags that select read-only or write mode.
17+
func addWriteModeFlags(cmd *cobra.Command, allowWrite, readOnly *bool, allowWriteUsage string) {
18+
cmd.Flags().BoolVar(allowWrite, "allow-write", false, allowWriteUsage+" Also read from "+allowWriteEnvVar+"; the flag wins.")
19+
// Users arriving from other MCP servers type --read-only reflexively. It is
20+
// already the default, so accept it rather than failing on an unknown flag.
21+
cmd.Flags().BoolVar(readOnly, "read-only", false, "Run without write actions. This is the default; the flag is accepted so it can be passed explicitly, and wins over --allow-write.")
22+
}
23+
24+
// resolveAllowWrite decides whether write actions are enabled.
25+
//
26+
// --read-only wins over everything so an explicit request for a safe session is
27+
// never overridden; otherwise --allow-write wins over the environment variable,
28+
// which is the more distant and easier-to-forget setting.
29+
func resolveAllowWrite(allowWriteFlag, allowWriteFlagSet, readOnly bool, envValue string) bool {
30+
if readOnly {
31+
return false
32+
}
33+
if allowWriteFlagSet {
34+
return allowWriteFlag
35+
}
36+
enabled, err := strconv.ParseBool(envValue)
37+
if err != nil {
38+
return false
39+
}
40+
return enabled
41+
}

‎pkg/cmd/outpost_mcp.go‎

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,13 @@ package cmd
33
import (
44
"context"
55
"os"
6-
"strconv"
76

87
"github.com/spf13/cobra"
98

109
outpostmcp "github.com/hookdeck/hookdeck-cli/pkg/outpost/mcp"
1110
"github.com/hookdeck/hookdeck-cli/pkg/validators"
1211
)
1312

14-
// allowWriteEnvVar enables write actions without a flag, for MCP clients whose
15-
// config makes environment variables easier to set than arguments.
16-
const allowWriteEnvVar = "HOOKDECK_MCP_ALLOW_WRITE"
17-
1813
// publishAPIKeyEnvVar carries the Project API key the publish tool needs.
1914
//
2015
// It is deliberately distinct from HOOKDECK_API_KEY. That variable means
@@ -78,10 +73,8 @@ before the server runs go to stderr.`),
7873
RunE: mc.runOutpostMCPCmd,
7974
}
8075

81-
mc.cmd.Flags().BoolVar(&mc.allowWrite, "allow-write", false, "Enable tools that create, change or delete data, and that return tenant credentials. Also read from "+allowWriteEnvVar+"; the flag wins.")
82-
// Users arriving from other MCP servers type --read-only reflexively. It is
83-
// already the default, so accept it rather than failing on an unknown flag.
84-
mc.cmd.Flags().BoolVar(&mc.readOnly, "read-only", false, "Run without write actions. This is the default; the flag is accepted so it can be passed explicitly, and wins over --allow-write.")
76+
addWriteModeFlags(mc.cmd, &mc.allowWrite, &mc.readOnly,
77+
"Enable tools that create, change or delete data, and that return tenant credentials.")
8578
// The env var is read at run time rather than used as the flag default, so a
8679
// key that is already in the environment is not printed back out by --help.
8780
mc.cmd.Flags().StringVar(&mc.apiKey, "publish-api-key", "", "Hookdeck Project API key, required by the publish tool. Also read from "+publishAPIKeyEnvVar+". HOOKDECK_API_KEY is deliberately not used here.")
@@ -93,25 +86,6 @@ func addOutpostMCPCmdTo(parent *cobra.Command) {
9386
parent.AddCommand(newOutpostMCPCmd().cmd)
9487
}
9588

96-
// resolveAllowWrite decides whether write actions are enabled.
97-
//
98-
// --read-only wins over everything so an explicit request for a safe session is
99-
// never overridden; otherwise --allow-write wins over the environment variable,
100-
// which is the more distant and easier-to-forget setting.
101-
func resolveAllowWrite(allowWriteFlag, allowWriteFlagSet, readOnly bool, envValue string) bool {
102-
if readOnly {
103-
return false
104-
}
105-
if allowWriteFlagSet {
106-
return allowWriteFlag
107-
}
108-
enabled, err := strconv.ParseBool(envValue)
109-
if err != nil {
110-
return false
111-
}
112-
return enabled
113-
}
114-
11589
func (mc *outpostMCPCmd) runOutpostMCPCmd(cmd *cobra.Command, args []string) error {
11690
// Always build the client — it may have an empty APIKey if the CLI is not
11791
// yet authenticated. The server handles that by registering outpost_login

0 commit comments

Comments
 (0)