Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 50 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,55 +614,81 @@ Claude Desktop (`claude_desktop_config.json`):

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.

#### Read-only by default

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:

```json
{
"mcpServers": {
"hookdeck": {
"command": "hookdeck",
"args": ["gateway", "mcp", "--allow-write"]
}
}
}
```

`--read-only` is accepted explicitly and wins if both are passed.

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.

#### Available tools

| Tool | Description |
|------|-------------|
| `hookdeck_projects` | List projects or switch the active project for this session |
| `hookdeck_connections` | Inspect connections and control delivery flow (list, get, pause, unpause) |
| `hookdeck_sources` | Inspect inbound sources (HTTP endpoints that receive events) |
| `hookdeck_destinations` | Inspect delivery destinations (HTTP endpoints where events are sent) |
| `hookdeck_transformations` | Inspect JavaScript transformations applied to event payloads |
| `hookdeck_requests` | Query inbound requests — list, get details, raw body, linked events |
| `hookdeck_events` | Query processed events — list, get details, raw payload body |
| `hookdeck_attempts` | Query delivery attempts — retry history, response codes, errors |
| `hookdeck_issues` | Inspect aggregated failure signals (delivery failures, transform errors, backpressure) |
| `hookdeck_metrics` | Query aggregate metrics — counts, failure rates, queue depth over time |
| `hookdeck_help` | Discover available tools and their actions |

`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.
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`.

| Tool | Read actions | Added by `--allow-write` |
|------|--------------|--------------------------|
| `hookdeck_projects` | list, use | — |
| `hookdeck_login` | (sign in) | — |
| `gateway_connections` | list, get, pause, unpause | create, upsert, update, delete, enable, disable |
| `gateway_sources` | list, get | create, upsert, update, delete, enable, disable |
| `gateway_destinations` | list, get | create, upsert, update, delete, enable, disable |
| `gateway_transformations` | list, get | create, upsert, update, delete, run |
| `gateway_requests` | list, get, raw_body, events, ignored_events | retry |
| `gateway_events` | list, get, raw_body | retry, cancel, mute |
| `gateway_attempts` | list, get | — |
| `gateway_issues` | list, get | update, dismiss |
| `gateway_metrics` | events, requests, attempts, transformations | — |
| `gateway_help` | overview, per-tool topics | — |

`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.

`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.

`gateway_help` reports which mode the session is in and lists only the actions it can perform.

#### Example prompts

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

```
"Are any of my events failing right now?"
→ Agent uses hookdeck_issues to list open issues, then hookdeck_events to inspect recent failures.
→ Agent uses gateway_issues to list open issues, then gateway_events to inspect recent failures.

"Show me the last 10 events for my Stripe source and check if any failed."
→ Agent uses hookdeck_sources to find the Stripe source, then hookdeck_events filtered by source and status.
→ Agent uses gateway_sources to find the Stripe source, then gateway_events filtered by source and status.

"What's the error rate for my API destination over the last 24 hours?"
→ Agent uses hookdeck_metrics with measures like failed_count and count, grouped by destination.
→ Agent uses gateway_metrics with measures like failed_count and count, grouped by destination.

"Trace request req_abc123 — what events did it produce, and did they all deliver successfully?"
→ Agent uses hookdeck_requests to get the request, then the events action to list generated events.
→ Agent uses gateway_requests to get the request, then the events action to list generated events.

"Why is my checkout endpoint returning 500s? Show me the latest attempt details."
→ Agent uses hookdeck_events filtered by status FAILED, then hookdeck_attempts to inspect delivery details.
→ Agent uses gateway_events filtered by status FAILED, then gateway_attempts to inspect delivery details.

"Pause the connection between Stripe and my staging endpoint while I debug."
→ Agent uses hookdeck_connections to find and pause the connection.
→ Agent uses gateway_connections to find and pause the connection.

"Compare failure rates across all my destinations this week."
→ Agent uses hookdeck_metrics with dimensions set to destination_id and measures like error_rate.
→ Agent uses gateway_metrics with dimensions set to destination_id and measures like error_rate.

"Find Stripe charge.succeeded events from the last week."
→ Agent uses hookdeck_events list with body filter {"type":"charge.succeeded"} and created_after / created_before ISO datetimes.
→ Agent uses gateway_events list with body filter {"type":"charge.succeeded"} and created_after / created_before ISO datetimes.

"Show failed events that had delivery attempts in the last 24 hours."
→ Agent uses hookdeck_events list with status FAILED and last_attempt_after set to yesterday's ISO datetime.
→ Agent uses gateway_events list with status FAILED and last_attempt_after set to yesterday's ISO datetime.
```

### Outpost
Expand Down
40 changes: 38 additions & 2 deletions pkg/cmd/mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"context"
"os"

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

type mcpCmd struct {
cmd *cobra.Command

allowWrite bool
readOnly bool
}

func newMCPCmd() *mcpCmd {
Expand All @@ -24,6 +28,19 @@ The server exposes Hookdeck Event Gateway resources — connections, sources,
destinations, events, requests, and more — as MCP tools that AI agents and
LLM-based clients can invoke.

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 to
enable creating, changing and deleting.

Pausing and unpausing a connection are available in both modes. Stopping a
misbehaving connection is the natural end of an investigation, and both are
reversible: pausing buffers delivery rather than dropping events.

Product tools are prefixed gateway_, so this server and 'hookdeck outpost mcp'
can be configured in the same client. Signing in and switching project are
Hookdeck operations rather than Event Gateway ones, so they keep the platform
prefix: hookdeck_login and hookdeck_projects.

If the CLI is already authenticated, all tools are available immediately.
If not, gateway MCP still starts: project selection is skipped until you
authenticate, and hookdeck_login initiates browser-based sign-in. Protocol
Expand All @@ -32,13 +49,20 @@ the server runs go to stderr.

hookdeck_login stays registered after sign-in so you can call it with reauth: true
to replace credentials (e.g. when project listing fails with a narrow API key).`),
Example: ` # Start the MCP server (stdio transport)
Example: ` # Start the MCP server, read-only (stdio transport)
hookdeck gateway mcp

# Allow tools that change data
hookdeck gateway mcp --allow-write

# Pipe a JSON-RPC initialize request for testing
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"test","version":"1.0"},"capabilities":{}}}' | hookdeck gateway mcp`,
RunE: mc.runMCPCmd,
}

addWriteModeFlags(mc.cmd, &mc.allowWrite, &mc.readOnly,
"Enable tools that create, change or delete data.")

return mc
}

Expand All @@ -51,6 +75,18 @@ func (mc *mcpCmd) runMCPCmd(cmd *cobra.Command, args []string) error {
// not yet authenticated. The MCP server handles this gracefully by
// registering a hookdeck_login tool instead of crashing.
client := Config.GetAPIClient()
srv := gatewaymcp.NewServer(client, &Config)

writeEnabled := resolveAllowWrite(
mc.allowWrite,
cmd.Flags().Changed("allow-write"),
mc.readOnly,
os.Getenv(allowWriteEnvVar),
)

srv := gatewaymcp.NewServer(gatewaymcp.ServerOptions{
Client: client,
Config: &Config,
WriteEnabled: writeEnabled,
})
return srv.RunStdio(context.Background())
}
41 changes: 41 additions & 0 deletions pkg/cmd/mcp_write_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cmd

import (
"strconv"

"github.com/spf13/cobra"
)

// allowWriteEnvVar enables write actions without a flag, for MCP clients whose
// config makes environment variables easier to set than arguments.
//
// It is shared by every MCP server the CLI starts: a user who wants write mode
// should not have to learn a different variable per product.
const allowWriteEnvVar = "HOOKDECK_MCP_ALLOW_WRITE"

// addWriteModeFlags registers the flags that select read-only or write mode.
func addWriteModeFlags(cmd *cobra.Command, allowWrite, readOnly *bool, allowWriteUsage string) {
cmd.Flags().BoolVar(allowWrite, "allow-write", false, allowWriteUsage+" Also read from "+allowWriteEnvVar+"; the flag wins.")
// Users arriving from other MCP servers type --read-only reflexively. It is
// already the default, so accept it rather than failing on an unknown flag.
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.")
}

// resolveAllowWrite decides whether write actions are enabled.
//
// --read-only wins over everything so an explicit request for a safe session is
// never overridden; otherwise --allow-write wins over the environment variable,
// which is the more distant and easier-to-forget setting.
func resolveAllowWrite(allowWriteFlag, allowWriteFlagSet, readOnly bool, envValue string) bool {
if readOnly {
return false
}
if allowWriteFlagSet {
return allowWriteFlag
}
enabled, err := strconv.ParseBool(envValue)
if err != nil {
return false
}
return enabled
}
30 changes: 2 additions & 28 deletions pkg/cmd/outpost_mcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@ package cmd
import (
"context"
"os"
"strconv"

"github.com/spf13/cobra"

outpostmcp "github.com/hookdeck/hookdeck-cli/pkg/outpost/mcp"
"github.com/hookdeck/hookdeck-cli/pkg/validators"
)

// allowWriteEnvVar enables write actions without a flag, for MCP clients whose
// config makes environment variables easier to set than arguments.
const allowWriteEnvVar = "HOOKDECK_MCP_ALLOW_WRITE"

// publishAPIKeyEnvVar carries the Project API key the publish tool needs.
//
// It is deliberately distinct from HOOKDECK_API_KEY. That variable means
Expand Down Expand Up @@ -78,10 +73,8 @@ before the server runs go to stderr.`),
RunE: mc.runOutpostMCPCmd,
}

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.")
// Users arriving from other MCP servers type --read-only reflexively. It is
// already the default, so accept it rather than failing on an unknown flag.
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.")
addWriteModeFlags(mc.cmd, &mc.allowWrite, &mc.readOnly,
"Enable tools that create, change or delete data, and that return tenant credentials.")
// The env var is read at run time rather than used as the flag default, so a
// key that is already in the environment is not printed back out by --help.
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.")
Expand All @@ -93,25 +86,6 @@ func addOutpostMCPCmdTo(parent *cobra.Command) {
parent.AddCommand(newOutpostMCPCmd().cmd)
}

// resolveAllowWrite decides whether write actions are enabled.
//
// --read-only wins over everything so an explicit request for a safe session is
// never overridden; otherwise --allow-write wins over the environment variable,
// which is the more distant and easier-to-forget setting.
func resolveAllowWrite(allowWriteFlag, allowWriteFlagSet, readOnly bool, envValue string) bool {
if readOnly {
return false
}
if allowWriteFlagSet {
return allowWriteFlag
}
enabled, err := strconv.ParseBool(envValue)
if err != nil {
return false
}
return enabled
}

func (mc *outpostMCPCmd) runOutpostMCPCmd(cmd *cobra.Command, args []string) error {
// Always build the client — it may have an empty APIKey if the CLI is not
// yet authenticated. The server handles that by registering outpost_login
Expand Down
Loading